--- name: react-patterns description: Use when implementing React 19 features, optimizing components, or choosing between Actions vs TanStack Query for mutations. Covers React Compiler optimization, Server Actions, Forms, and new hooks. updated: 2026-01-20 keywords: react-19, react-compiler, server-actions, forms, hooks, use-hook, optimistic-updates --- # React 19 Patterns and Best Practices Modern React 19 patterns leveraging the React Compiler, Server Actions, and new hooks. ## Compiler-Friendly Code The React Compiler automatically optimizes components for performance. Write code that works well with it: **Best Practices:** - Keep components pure and props serializable - Derive values during render (don't stash in refs unnecessarily) - Keep event handlers inline unless they close over large mutable objects - Verify compiler is working (DevTools ✨ badge) - Opt-out problematic components with `"use no memo"` while refactoring **Example - Pure Component:** ```typescript // ✅ Compiler-friendly - pure function function UserCard({ user }: { user: User }) { const displayName = `${user.firstName} ${user.lastName}` const isVIP = user.points > 1000 return (

{displayName}

{isVIP && VIP}
) } // ❌ Avoid - unnecessary effects function UserCard({ user }: { user: User }) { const [displayName, setDisplayName] = useState('') useEffect(() => { setDisplayName(`${user.firstName} ${user.lastName}`) }, [user]) return

{displayName}

} ``` **Verification:** - Open React DevTools - Look for "Memo ✨" badge on components - If missing, component wasn't optimized (check for violations) **Opt-Out When Needed:** ```typescript 'use no memo' // Component code that can't be optimized yet function ProblematicComponent() { // ... code with compiler issues } ``` ## Actions & Forms For SPA mutations, choose **one approach per feature**: - **React 19 Actions:** `
`, `useActionState`, `useOptimistic` - **TanStack Query:** `useMutation` Don't duplicate logic between both approaches. ### React 19 Actions (Form-Centric) **Best for:** - Form submissions - Simple CRUD operations - When you want form validation built-in **Basic Action:** ```typescript 'use server' // Only if using SSR/RSC, omit for SPA async function createTodoAction(formData: FormData) { const text = formData.get('text') as string // Validation if (!text || text.length < 3) { return { error: 'Text must be at least 3 characters' } } // API call await api.post('/todos', { text }) // Revalidation happens automatically return { success: true } } // Component function TodoForm() { return (
) } ``` **With State (useActionState):** ```typescript import { useActionState } from 'react' function TodoForm() { const [state, formAction, isPending] = useActionState( createTodoAction, { error: null, success: false } ) return (
{state.error && {state.error}}
) } ``` **With Optimistic Updates (useOptimistic):** ```typescript import { useOptimistic } from 'react' function TaskList({ initialTodos }: { initialTodos: Todo[] }) { const [optimisticTodos, addOptimisticTodo] = useOptimistic( initialTodos, (state, newTodo: string) => [ ...state, { id: `temp-${Date.now()}`, text: newTodo, completed: false } ] ) async function handleSubmit(formData: FormData) { const text = formData.get('text') as string addOptimisticTodo(text) await createTodoAction(formData) } return ( <>
) } ``` ### TanStack Query Mutations (Preferred for SPAs) **Best for:** - Non-form mutations (e.g., button clicks) - Complex optimistic updates with rollback - Integration with existing Query cache - More control over caching and invalidation See **tanstack-query** skill for comprehensive mutation patterns. **Quick Example:** ```typescript import { useMutation, useQueryClient } from '@tanstack/react-query' function useCre ateTodo() { const queryClient = useQueryClient() return useMutation({ mutationFn: (text: string) => api.post('/todos', { text }), onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['todos'] }) }, }) } // Usage function TodoForm() { const createTodo = useCreateTodo() return (
{ e.preventDefault() const formData = new FormData(e.currentTarget) createTodo.mutate(formData.get('text') as string) }}>
) } ``` ## The `use` Hook The `use` hook unwraps Promises and Context, enabling new patterns. **With Promises:** ```typescript import { use, Suspense } from 'react' function UserProfile({ userPromise }: { userPromise: Promise }) { const user = use(userPromise) return
{user.name}
} // Usage function App() { const userPromise = fetchUser(1) return ( }> ) } ``` **With Context:** ```typescript import { use, createContext } from 'react' const ThemeContext = createContext('light') function Button() { const theme = use(ThemeContext) return } ``` **When to Use:** - Primarily useful with Suspense/data primitives and RSC (React Server Components) - **For SPA-only apps**, prefer **TanStack Query + Router loaders** for data fetching - `use` shines when you already have a Promise from a parent component ## Component Composition Patterns **Compound Components:** ```typescript // ✅ Good - composable, flexible Dashboard {/* content */} // Implementation function Card({ children }: { children: React.ReactNode }) { return
{children}
} Card.Header = function CardHeader({ children }: { children: React.ReactNode }) { return
{children}
} Card.Title = function CardTitle({ children }: { children: React.ReactNode }) { return

{children}

} Card.Content = function CardContent({ children }: { children: React.ReactNode }) { return
{children}
} ``` **Render Props (when needed):** ```typescript function DataLoader({ fetch, render }: { fetch: () => Promise render: (data: T) => React.ReactNode }) { const { data } = useQuery({ queryKey: ['data'], queryFn: fetch }) if (!data) return return <>{render(data)} } // Usage fetchUser(1)} render={(user) => } /> ``` ## Error Boundaries React 19 still requires class components for error boundaries (or use a library): ```typescript import { Component, ReactNode } from 'react' class ErrorBoundary extends Component< { children: ReactNode; fallback: ReactNode }, { hasError: boolean } > { state = { hasError: false } static getDerivedStateFromError() { return { hasError: true } } componentDidCatch(error: Error, info: { componentStack: string }) { console.error('Error caught:', error, info) } render() { if (this.state.hasError) { return this.props.fallback } return this.props.children } } // Usage }> ``` **Or use react-error-boundary library:** ```typescript import { ErrorBoundary } from 'react-error-boundary' Something went wrong} onError={(error, info) => console.error(error, info)} > ``` ## Decision Guide: Actions vs Query Mutations | Scenario | Recommendation | |----------|---------------| | Form submission with validation | React Actions | | Button click mutation | TanStack Query | | Needs optimistic updates + rollback | TanStack Query | | Integrates with existing cache | TanStack Query | | SSR/RSC application | React Actions | | SPA with complex data flow | TanStack Query | | Simple CRUD with forms | React Actions | **Rule of Thumb:** For SPAs with TanStack Query already in use, prefer Query mutations for consistency. Only use Actions for form-heavy features where the form-centric API is beneficial. ## Related Skills - **tanstack-query** - Server state with mutations and optimistic updates - **core-principles** - Overall project structure - **tooling-setup** - React Compiler configuration