React 19 with TypeScript 5.9.3, TanStack Query V5, Zustand, and modern patterns. React Compiler handles memoization automatically. No manual optimization needed.
Modern React 19 patterns with TypeScript 5.9.3, TanStack Query V5, Zustand 5, and 2025 best practices. React Compiler handles optimization automatically.
Auto-activates when:
react, typescript, useQuery, useMutation, zustand, zod, componentfrontend/src/, components/, hooks/, pages/, *.tsx, *.tsNOT for:
fastapi-patterns skilldeepagents-integration skillfastapi-patterns skill// 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>
);
}
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
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:
Breaking changes from V4:
isLoading ā isPendingcacheTime ā gcTimeonSuccess/onError removed from queriesqueryKey must 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:
'agents' to ['agents']gcTime insteadmutateAsync().then() in component// 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:
resolver: zodResolver(schema) is sethandleSubmit wraps your handlerz.infer<typeof schema> for form data type// 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>
);
}
// 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>
);
}
isPending, gcTime, array queryKeys)React.memo(), useMemo(), useCallback() unless profiled as necessary