| name | react-enterprise |
| description | React 19 with TypeScript 5.9.3, TanStack Query V5, Zustand, and modern patterns. React Compiler handles memoization automatically. No manual optimization needed. |
| triggers | [object Object] |
React Enterprise Patterns Skill
Modern React 19 patterns with TypeScript 5.9.3, TanStack Query V5, Zustand 5, and 2025 best practices. React Compiler handles optimization automatically.
🎯 When to Use This Skill
Auto-activates when:
- Keywords:
react,typescript,useQuery,useMutation,zustand,zod,component - Files:
frontend/src/,components/,hooks/,pages/,*.tsx,*.ts - Tasks: creating components, state management, data fetching, form handling, TypeScript types
NOT for:
- Backend API development → use
fastapi-patternsskill - Agent integration → use
deepagents-integrationskill - Database operations → use
fastapi-patternsskill
⚡ Quick Reference
Top 10 Essential Patterns
// 1. Modern Component (React 19 - No memoization needed)
import type { Agent } from '@/types/agent';
interface AgentCardProps {
agent: Agent;
onEdit?: (id: number) => void;
}
export function AgentCard({ agent, onEdit }: AgentCardProps) {
// ✅ No useCallback needed - React Compiler handles it
const handleEdit = () => onEdit?.(agent.id);
return <div onClick={handleEdit}>{agent.name}</div>;
}
// 2. TanStack Query V5 - Data Fetching
import { useQuery } from '@tanstack/react-query';
export function useAgents() {
return useQuery({
queryKey: ['agents'], // Must be array (V5 requirement)
queryFn: agentsApi.getAll,
});
}
const { data, isPending, error } = useAgents(); // Note: isPending not isLoading
// 3. Mutation with Optimistic Updates
import { useMutation, useQueryClient } from '@tanstack/react-query';
export function useCreateAgent() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (data: AgentCreate) => agentsApi.create(data),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['agents'] });
},
});
}
// 4. Form with React Hook Form + Zod
import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import { z } from 'zod';
const agentSchema = z.object({
name: z.string().min(1).max(100),
model: z.string(),
temperature: z.number().min(0).max(2),
});
type AgentFormData = z.infer<typeof agentSchema>;
function AgentForm() {
const { register, handleSubmit, formState: { errors } } = useForm<AgentFormData>({
resolver: zodResolver(agentSchema),
});
const onSubmit = (data: AgentFormData) => {
console.log(data);
};
return (
<form onSubmit={handleSubmit(onSubmit)}>
<input {...register('name')} />
{errors.name && <span>{errors.name.message}</span>}
</form>
);
}
// 5. Zustand Store (Modern V5)
import { create } from 'zustand';
import { devtools } from 'zustand/middleware';
interface UIStore {
sidebarOpen: boolean;
toggleSidebar: () => void;
}
export const useUIStore = create<UIStore>()(
devtools((set) => ({
sidebarOpen: true,
toggleSidebar: () => set((state) => ({ sidebarOpen: !state.sidebarOpen })),
}))
);
// 6. WebSocket Hook
import { useEffect, useState } from 'react';
import { io, Socket } from 'socket.io-client';
export function useWebSocket(url: string) {
const [socket, setSocket] = useState<Socket | null>(null);
const [connected, setConnected] = useState(false);
useEffect(() => {
const newSocket = io(url);
newSocket.on('connect', () => setConnected(true));
newSocket.on('disconnect', () => setConnected(false));
setSocket(newSocket);
return () => {
newSocket.close();
};
}, [url]);
return { socket, connected };
}
// 7. Type-Safe API Client
import axios from 'axios';
const apiClient = axios.create({
baseURL: '/api/v1',
timeout: 30000,
});
// Request interceptor for auth
apiClient.interceptors.request.use((config) => {
const token = localStorage.getItem('auth_token');
if (token) {
config.headers.Authorization = `Bearer ${token}`;
}
return config;
});
// 8. Const Assertions (No Enums)
// ❌ WRONG: enum (creates runtime code)
enum Status { Active = 'active' }
// ✅ CORRECT: const assertion (zero runtime)
const Status = { Active: 'active', Inactive: 'inactive' } as const;
type Status = typeof Status[keyof typeof Status];
// 9. Error Boundary
import { Component, type ReactNode, type ErrorInfo } from 'react';
interface Props {
children: ReactNode;
fallback?: ReactNode;
}
interface State {
hasError: boolean;
error?: Error;
}
export class ErrorBoundary extends Component<Props, State> {
state: State = { hasError: false };
static getDerivedStateFromError(error: Error): State {
return { hasError: true, error };
}
componentDidCatch(error: Error, info: ErrorInfo) {
console.error('Error caught:', error, info);
}
render() {
if (this.state.hasError) {
return this.props.fallback || <div>Something went wrong</div>;
}
return this.props.children;
}
}
// 10. Conditional Rendering Pattern
// ✅ CORRECT: Early return pattern
function AgentList({ agents }: { agents: Agent[] }) {
if (agents.length === 0) {
return <EmptyState />;
}
return (
<div>
{agents.map((agent) => (
<AgentCard key={agent.id} agent={agent} />
))}
</div>
);
}
📁 Project Structure
frontend/src/
├── components/ # Reusable components
│ ├── agents/ # Agent-specific components
│ │ ├── AgentCard.tsx # Agent card display
│ │ ├── AgentForm.tsx # Agent creation/edit form
│ │ └── AgentList.tsx # Agent list with filters
│ ├── common/ # Shared UI components
│ │ ├── Button.tsx # Reusable button
│ │ ├── Input.tsx # Form input
│ │ └── Modal.tsx # Modal dialog
│ └── layout/ # Layout components
│ ├── Header.tsx # App header
│ └── Sidebar.tsx # Navigation sidebar
├── hooks/ # Custom React hooks
│ ├── useAgents.ts # Agent data fetching
│ ├── useWebSocket.ts # WebSocket connection
│ └── useAuth.ts # Authentication state
├── pages/ # Route pages
│ ├── Dashboard.tsx # Main dashboard
│ ├── AgentStudio.tsx # Agent configuration
│ └── Analytics.tsx # Analytics page
├── stores/ # Zustand stores
│ ├── uiStore.ts # UI state (sidebar, theme)
│ └── authStore.ts # Auth state (user, token)
├── api/ # API client layer
│ ├── client.ts # Axios instance + interceptors
│ └── agents.ts # Agent API methods
├── types/ # TypeScript types
│ ├── agent.ts # Agent types
│ └── user.ts # User types
├── utils/ # Utility functions
│ └── formatters.ts # Date, number formatters
├── App.tsx # Root component
└── main.tsx # Entry point
🔧 Core Patterns
1. Modern Component Pattern (React 19)
No manual memoization - React Compiler handles optimization
import type { Agent } from '@/types/agent';
interface AgentCardProps {
agent: Agent;
onEdit?: (id: number) => void;
onDelete?: (id: number) => void;
}
// ✅ CORRECT: No React.memo, no useCallback, no useMemo
export function AgentCard({ agent, onEdit, onDelete }: AgentCardProps) {
// React Compiler optimizes these automatically
const handleEdit = () => onEdit?.(agent.id);
const handleDelete = () => {
if (confirm('Delete this agent?')) {
onDelete?.(agent.id);
}
};
// Derived state - no useMemo needed
const statusColor = agent.is_active ? 'green' : 'gray';
return (
<div className="border rounded-lg p-4">
<h3 className="text-lg font-semibold">{agent.name}</h3>
<p className="text-sm text-gray-600">{agent.description}</p>
<div className="flex gap-2 mt-4">
<button onClick={handleEdit}>Edit</button>
<button onClick={handleDelete}>Delete</button>
</div>
<span className={`text-${statusColor}-500`}>
{agent.is_active ? 'Active' : 'Inactive'}
</span>
</div>
);
}
// ❌ WRONG: Over-optimization (unnecessary in React 19)
export const AgentCard = React.memo(({ agent, onEdit }: AgentCardProps) => {
const handleEdit = useCallback(() => onEdit?.(agent.id), [onEdit, agent.id]);
const statusColor = useMemo(
() => agent.is_active ? 'green' : 'gray',
[agent.is_active]
);
// ... This is now unnecessary complexity!
});
Troubleshooting:
- Re-rendering issues → React Compiler handles this; check if you're mutating props
- Performance still slow → Profile first, optimize only critical paths manually
- When to use manual memo → Only for expensive computations (>50ms)
2. TanStack Query V5 Pattern
Breaking changes from V4:
isLoading→isPendingcacheTime→gcTimeonSuccess/onErrorremoved from queriesqueryKeymust be array
// hooks/useAgents.ts
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { agentsApi } from '@/api/agents';
import type { AgentCreate, AgentUpdate } from '@/types/agent';
// ✅ CORRECT: V5 syntax
export function useAgents() {
return useQuery({
queryKey: ['agents'], // Array required
queryFn: agentsApi.getAll,
staleTime: 5 * 60 * 1000, // 5 minutes
gcTime: 10 * 60 * 1000, // Note: gcTime not cacheTime
});
}
export function useAgent(id: number) {
return useQuery({
queryKey: ['agents', id],
queryFn: () => agentsApi.getById(id),
enabled: !!id, // Only fetch if id exists
});
}
export function useCreateAgent() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (data: AgentCreate) => agentsApi.create(data),
onSuccess: () => {
// Invalidate cache to refetch
queryClient.invalidateQueries({ queryKey: ['agents'] });
},
// 🔒 Error handling in component, not here
});
}
export function useUpdateAgent() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: ({ id, data }: { id: number; data: AgentUpdate }) =>
agentsApi.update(id, data),
onSuccess: (_, variables) => {
queryClient.invalidateQueries({ queryKey: ['agents'] });
queryClient.invalidateQueries({ queryKey: ['agents', variables.id] });
},
});
}
// Usage in component
function AgentList() {
const { data: agents, isPending, error } = useAgents(); // Note: isPending
const createMutation = useCreateAgent();
if (isPending) return <div>Loading...</div>;
if (error) return <div>Error: {error.message}</div>;
const handleCreate = async (data: AgentCreate) => {
try {
await createMutation.mutateAsync(data);
alert('Agent created!');
} catch (err) {
alert('Failed to create agent');
}
};
return <div>{/* ... */}</div>;
}
// ❌ WRONG: V4 syntax (will error in V5)
export function useAgentsOld() {
return useQuery(
'agents', // String key not allowed
agentsApi.getAll,
{
cacheTime: 10000, // Property doesn't exist
onSuccess: () => {}, // Removed from queries
}
);
}
Troubleshooting:
- "queryKey must be array" → Change
'agents'to['agents'] - "cacheTime not found" → Use
gcTimeinstead - onSuccess not working → Move to
mutateAsync().then()in component
3. Form Pattern (React Hook Form + Zod)
// components/agents/AgentForm.tsx
import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import { z } from 'zod';
import type { AgentCreate } from '@/types/agent';
// Zod schema with validation
const agentSchema = z.object({
name: z.string().min(1, 'Name required').max(100, 'Name too long'),
description: z.string().max(500).optional(),
model_name: z.string().regex(/^(claude|gpt)-/, 'Invalid model'),
temperature: z.number().min(0).max(2).default(0.7),
max_tokens: z.number().int().min(1).max(100000).default(4096),
planning_enabled: z.boolean().default(true),
});
type AgentFormData = z.infer<typeof agentSchema>;
interface AgentFormProps {
initialData?: Partial<AgentFormData>;
onSubmit: (data: AgentFormData) => void;
onCancel?: () => void;
}
export function AgentForm({ initialData, onSubmit, onCancel }: AgentFormProps) {
const {
register,
handleSubmit,
formState: { errors, isSubmitting },
reset,
} = useForm<AgentFormData>({
resolver: zodResolver(agentSchema),
defaultValues: initialData || {
temperature: 0.7,
max_tokens: 4096,
planning_enabled: true,
},
});
const handleFormSubmit = async (data: AgentFormData) => {
try {
await onSubmit(data);
reset();
} catch (error) {
console.error('Form submission error:', error);
}
};
return (
<form onSubmit={handleSubmit(handleFormSubmit)} className="space-y-4">
{/* Name field */}
<div>
<label htmlFor="name" className="block text-sm font-medium">
Name *
</label>
<input
id="name"
{...register('name')}
className="mt-1 block w-full rounded-md border-gray-300"
/>
{errors.name && (
<p className="mt-1 text-sm text-red-600">{errors.name.message}</p>
)}
</div>
{/* Description field */}
<div>
<label htmlFor="description" className="block text-sm font-medium">
Description
</label>
<textarea
id="description"
{...register('description')}
rows={3}
className="mt-1 block w-full rounded-md border-gray-300"
/>
</div>
{/* Model selection */}
<div>
<label htmlFor="model_name" className="block text-sm font-medium">
Model *
</label>
<select
id="model_name"
{...register('model_name')}
className="mt-1 block w-full rounded-md border-gray-300"
>
<option value="claude-sonnet-4-5-20250929">Claude Sonnet 4.5</option>
<option value="claude-haiku-4-20250508">Claude Haiku 4</option>
<option value="gpt-4-turbo">GPT-4 Turbo</option>
</select>
{errors.model_name && (
<p className="mt-1 text-sm text-red-600">{errors.model_name.message}</p>
)}
</div>
{/* Temperature slider */}
<div>
<label htmlFor="temperature" className="block text-sm font-medium">
Temperature: {/* Display current value */}
</label>
<input
id="temperature"
type="range"
min="0"
max="2"
step="0.1"
{...register('temperature', { valueAsNumber: true })}
className="mt-1 block w-full"
/>
</div>
{/* Planning checkbox */}
<div className="flex items-center">
<input
id="planning_enabled"
type="checkbox"
{...register('planning_enabled')}
className="h-4 w-4 rounded border-gray-300"
/>
<label htmlFor="planning_enabled" className="ml-2 text-sm">
Enable planning system
</label>
</div>
{/* Actions */}
<div className="flex gap-2">
<button
type="submit"
disabled={isSubmitting}
className="px-4 py-2 bg-blue-600 text-white rounded-md"
>
{isSubmitting ? 'Saving...' : 'Save Agent'}
</button>
{onCancel && (
<button
type="button"
onClick={onCancel}
className="px-4 py-2 bg-gray-200 rounded-md"
>
Cancel
</button>
)}
</div>
</form>
);
}
Troubleshooting:
- Validation not working → Check
resolver: zodResolver(schema)is set - Form not submitting → Verify
handleSubmitwraps your handler - Type errors → Use
z.infer<typeof schema>for form data type
4. Zustand Store Pattern (V5)
// stores/uiStore.ts
import { create } from 'zustand';
import { devtools, persist } from 'zustand/middleware';
interface UIStore {
sidebarOpen: boolean;
theme: 'light' | 'dark';
notifications: Notification[];
toggleSidebar: () => void;
setTheme: (theme: 'light' | 'dark') => void;
addNotification: (notification: Notification) => void;
removeNotification: (id: string) => void;
}
// ✅ CORRECT: V5 syntax with middleware
export const useUIStore = create<UIStore>()(
devtools(
persist(
(set) => ({
sidebarOpen: true,
theme: 'light',
notifications: [],
toggleSidebar: () =>
set((state) => ({ sidebarOpen: !state.sidebarOpen })),
setTheme: (theme) => set({ theme }),
addNotification: (notification) =>
set((state) => ({
notifications: [...state.notifications, notification],
})),
removeNotification: (id) =>
set((state) => ({
notifications: state.notifications.filter((n) => n.id !== id),
})),
}),
{ name: 'ui-store' } // LocalStorage key
)
)
);
// Usage in components
function Sidebar() {
const { sidebarOpen, toggleSidebar } = useUIStore();
// 🔒 Only re-renders when sidebarOpen changes
return (
<div className={sidebarOpen ? 'open' : 'closed'}>
<button onClick={toggleSidebar}>Toggle</button>
</div>
);
}
// Selector for specific state
function ThemeToggle() {
const theme = useUIStore((state) => state.theme);
const setTheme = useUIStore((state) => state.setTheme);
return (
<button onClick={() => setTheme(theme === 'light' ? 'dark' : 'light')}>
{theme === 'light' ? '🌙' : '☀️'}
</button>
);
}
5. WebSocket Pattern
// hooks/useWebSocket.ts
import { useEffect, useState, useCallback } from 'react';
import { io, type Socket } from 'socket.io-client';
interface UseWebSocketReturn {
socket: Socket | null;
connected: boolean;
emit: (event: string, data: any) => void;
}
export function useWebSocket(url: string): UseWebSocketReturn {
const [socket, setSocket] = useState<Socket | null>(null);
const [connected, setConnected] = useState(false);
useEffect(() => {
const newSocket = io(url, {
reconnection: true,
reconnectionDelay: 1000,
reconnectionAttempts: 5,
});
newSocket.on('connect', () => {
console.log('WebSocket connected');
setConnected(true);
});
newSocket.on('disconnect', () => {
console.log('WebSocket disconnected');
setConnected(false);
});
setSocket(newSocket);
return () => {
newSocket.close();
};
}, [url]);
const emit = useCallback(
(event: string, data: any) => {
if (socket?.connected) {
socket.emit(event, data);
}
},
[socket]
);
return { socket, connected, emit };
}
// Usage in component
function ExecutionMonitor({ executionId }: { executionId: number }) {
const { socket, connected } = useWebSocket('http://localhost:8000');
const [progress, setProgress] = useState(0);
useEffect(() => {
if (!socket) return;
socket.emit('subscribe', { executionId });
socket.on('progress', (data) => {
setProgress(data.progress);
});
return () => {
socket.off('progress');
socket.emit('unsubscribe', { executionId });
};
}, [socket, executionId]);
return (
<div>
{connected ? '🟢 Connected' : '🔴 Disconnected'}
<div>Progress: {progress}%</div>
</div>
);
}
🎯 Best Practices Summary
DO ✅
- Use React Compiler - no manual memoization needed
- Use TanStack Query V5 syntax (
isPending,gcTime, array queryKeys) - Use Zod for schema validation
- Use const assertions instead of enums
- Use TypeScript strict mode
- Use error boundaries for error handling
- Use Zustand for global state
- Use React Hook Form for complex forms
DON'T ❌
- Don't use
React.memo(),useMemo(),useCallback()unless profiled as necessary - Don't use V4 TanStack Query syntax
- Don't use enums (use const assertions)
- Don't mutate state directly
- Don't skip TypeScript types
- Don't use any (use unknown and type guards)
📚 See Also
- reference.md - Complete React/TypeScript API reference
- examples.md - Full working component examples
- fastapi-patterns - Backend API integration
- deepagents-integration - Agent management integration