React 19 Best Practices
Comprehensive React 19/19.2 best-practices guide for AI agents. Contains 49 rules across 9 categories, prioritized by impact from critical (concurrent rendering, server components) through to cross-cutting codebase hygiene (dedup, dead code, boundary coherence). Reflects React 19 headline changes: ref as a regular prop (forwardRef deprecated), native document metadata, resource preload APIs, useActionState, useOptimistic, use() hook, and <Context> as provider.
Rule files describe pattern shapes (not API names) and open with a "Shapes to recognize" section listing 2–4 syntactic disguises the same break can wear. Selected high-value rules (those whose disguises are most common in real codebases — form actions, ref-as-prop, derived state, context, the use() hook, useCallback/memo pairing) include an extra concrete "In disguise" incorrect/correct example pair to teach pattern detection beyond the grep-friendly cases.
When to Apply
- Writing new React components or refactoring existing ones
- Auditing or modernizing a directory, PR, or whole repository (see
references/_review-algorithm.mdfor the required procedure) - Migrating from React 18 to React 19 (forwardRef → ref-as-prop,
<Context.Provider>→<Context>,useFormState→useActionState) - Optimizing re-render performance or bundle size
- Using concurrent features (useTransition, useDeferredValue, Activity)
- Setting up Server Components or server/client boundaries
- Implementing form actions, optimistic updates, or data fetching
- Configuring React Compiler for automatic memoization
- Reviewing React code for common anti-patterns or outdated React 18 idioms
- Finding codebase-level issues that single-file rules can't see: duplicated logic across files, near-duplicate components, dead code,
'use client'files that don't need the client, prop-shape drift (see Category 9)
How to Review or Refactor a Codebase
When the user asks to review, refactor, modernize, or audit React code — single file or whole repo — follow references/_review-algorithm.md. Do not improvise.
Four non-negotiables from that doc:
- Two modes — never refuse a whole-repo audit. Pick Mode A (scoped, ≤~20 files) or Mode B (whole-tree: inventory pass + targeted sweeps + full Category 9). The algorithm tells you how to handle "audit my codebase" without dumping 800 files into context.
- Judgment over grep. Each rule names a pattern shape, not a syntactic marker. Read each rule's Shapes to recognize section before sweeping — grep finds the easy violations and misses the high-value ones (a manually-drilled callback ref because the author dodged
forwardRef; anonSubmitdoing the work ofuseActionState; auseState+useEffectshaped like derived state; a custom hook hiding the fetch dance). Grep is a trigger, never a verdict. - Category-major, not file-major — with forcing functions. Sweep one category at a time across all in-scope files in priority order (CRITICAL → … → CROSS-CUTTING). The algorithm requires a scope declaration, per-category progress lines, and a final coverage table (
category × file, cells ∈{clean, N findings, n/a}). A missing category in the output is immediately visible. - Codebase-level findings come from Category 9. Single-file rules can't tell you "these two components should be one" or "this hook is dead." Category 9 (Codebase Hygiene) sweeps the full inventory at the end and produces remove / dedup / reuse / consolidate findings.
Single-file ad-hoc questions ("is this hook OK?") can go straight to the relevant rule. The algorithm exists for the multi-file and whole-repo cases.
Rule Categories
| # | Category | Impact | Rules | Key Topics | |---|----------|--------|-------|------------| | 1 | Concurrent Rendering | CRITICAL | 6 | useTransition, useDeferredValue, Activity, batching, render purity | | 2 | Server Components | CRITICAL | 6 | RSC boundaries, data fetching, streaming, serializable props | | 3 | Actions & Forms | HIGH | 5 | Form actions, declarative form state, useOptimistic, server validation | | 4 | Data Fetching | HIGH | 7 | use() hook, cache(), Suspense, document metadata, resource hints | | 5 | State Management | MEDIUM-HIGH | 5 | Derived values, context split, functional updates, reducer | | 6 | Memoization & Performance | MEDIUM | 5 | React Compiler, useMemo, useCallback, React.memo | | 7 | Effects & Events | MEDIUM | 5 | useEffectEvent, cleanup, external stores, derived-state anti-pattern | | 8 | Component Patterns | LOW-MEDIUM | 5 | ref-as-prop, composition, controlled vs uncontrolled, key reset | | 9 | Codebase Hygiene | CROSS-CUTTING | 5 | Dedup, consolidation, dead code, boundary coherence, prop-shape drift |
Quick Reference
Critical patterns — get these right first:
- Fetch data in Server Components, not Client Components
- Push
'use client'boundaries as low as possible - Use
startTransitionfor expensive non-blocking updates - Use
<Activity>to preserve state across tab/page switches
React 19 modern idioms (do NOT generate React 18 patterns):
function C({ ref, ...props })— never wrap inforwardRef<MyContext value={v}>— never use<MyContext.Provider>useActionState— never useuseFormStateuseRef<T>(null)— always pass an initial value- Render
<title>,<meta>,<link>inline — never reach forreact-helmet preload/preconnectfromreact-dom— never hand-render<link rel="preload">
Common single-file mistakes — avoid these anti-patterns:
- Creating promises inside Client Components for
use()(causes infinite loops) - Memoizing everything (use React Compiler v1.0+ instead)
- Using effects for derived state, mutations, parent notifications, or app init
- Placing
'use client'too high in the component tree
Codebase-level patterns — surface these in Category 9 sweeps:
- Components/hooks/utilities with zero importers — delete
- 2+ files with the same effect/state shape — extract a shared hook
- 2+ structurally identical components with drift in labels/icons — consolidate with variants or composition
'use client'files whose hook usage doesn't require client execution — demote to Server Components (or split into server parent + client island)- Same conceptual prop carried under different names across components — converge on a canonical name
Table of Contents
- Concurrent Rendering — CRITICAL
- 1.1 Preserve hidden subtree state across navigation instead of unmounting — HIGH (
<Activity>) - 1.2 Keep previous content visible across navigation by wrapping the update in a transition — HIGH
- 1.3 Trust automatic batching — don't reach for flushSync or unstable_batchedUpdates — HIGH
- 1.4 Defer a value you don't own when it drives an expensive child re-render — CRITICAL (
useDeferredValue) - 1.5 Mark expensive state updates as low-priority so input stays responsive — CRITICAL (
useTransition) - 1.6 Keep render pure — never mutate, subscribe, or read external state during render — MEDIUM-HIGH
- 1.1 Preserve hidden subtree state across navigation instead of unmounting — HIGH (
- Server Components — CRITICAL
- 2.1 Quarantine browser-API-dependent libraries inside Client Component wrappers — MEDIUM-HIGH
- 2.2 Split slow async work behind its own Suspense boundary so fast content streams first — MEDIUM-HIGH
- 2.3 Pull data on the server with async/await — never
useEffect+fetchin a Client Component — CRITICAL - 2.4 Push the
'use client'boundary down to the interactive leaf, not up at the route — CRITICAL - 2.5 Only data that the RSC wire format can encode crosses the server→client boundary — HIGH
- 2.6 Server content reaches inside a Client Component via
childrenor named slots, not by being imported — HIGH
- Actions & Forms — HIGH
- 3.1 Wire form submission through the
actionprop, not a JS-onlyonSubmithandler — HIGH - 3.2 Lift imperative pending/error/submit bookkeeping into a single declarative form-state hook — HIGH (
useActionState) - 3.3 Submit buttons read parent-form pending state from context, not from a prop drilled in — MEDIUM-HIGH (
useFormStatus) - 3.4 Show the post-mutation outcome immediately with automatic rollback on failure — HIGH (
useOptimistic) - 3.5 Treat validation as a server-action concern; client checks are an enhancement, never the only gate — MEDIUM
- 3.1 Wire form submission through the
- Data Fetching — HIGH
- 4.1 Independent fetches run concurrently — sequential
awaitis a waterfall — MEDIUM-HIGH - 4.2 Per-request memoize data fetchers so multiple components reading the same data don't re-fetch — HIGH (
cache()) - 4.3 Each Suspense boundary needs an Error Boundary wrapping it — failures must be containable — MEDIUM
- 4.4 Loading states are declared as Suspense fallbacks, not assembled from
if (loading) return …— HIGH - 4.5 Read promises and conditional context with
use()instead ofuseEffect+useStateplumbing — HIGH (use()) - 4.6 Page metadata renders as
<title>/<meta>/<link>inline — drop helmet-style head managers — MEDIUM - 4.7 Reach for the imperative resource-hint APIs from
react-dominstead of hand-rendering<link rel="preload">— MEDIUM
- 4.1 Independent fetches run concurrently — sequential
- State Management — MEDIUM-HIGH
- 5.1 Compute derived values in the render body — never mirror them into separate state — MEDIUM
- 5.2 Each context holds one independently-changing piece of state — split fat contexts apart — MEDIUM
- 5.3 When the next state depends on the previous, pass
setX(prev => …)— notsetX(x + 1)— MEDIUM-HIGH - 5.4 Pass a function to
useStatewhen the initial value is expensive — never compute it inline — MEDIUM-HIGH - 5.5 Coordinated multi-field state transitions live in a single reducer — not in N sibling
useStatecells — MEDIUM (useReducer)
- Memoization & Performance — MEDIUM
- 6.1 Memoize from a measured baseline — don't pre-wrap every value and callback in useMemo/useCallback — MEDIUM
- 6.2 Adopt React Compiler v1.0 — let the build memoize, then remove the manual
useMemo/useCallbacknoise — MEDIUM - 6.3 Wrap expensive pure components in
memo()only when their props are actually stable — MEDIUM - 6.4 Stabilize a callback's identity only when something downstream depends on identity stability — MEDIUM (
useCallback) - 6.5 Cache a computation between renders only when its inputs are stable and the work is measurably expensive — MEDIUM (
useMemo)
- Effects & Events — MEDIUM
- 7.1 Every effect that subscribes, schedules, or connects must return a cleanup that tears it down — MEDIUM
- 7.2
useEffectis for syncing with external systems — never for derived state, mutations, event logic, parent notification, or app init — HIGH - 7.3 Effect deps are reference-compared — pass primitives, not in-render-constructed objects or arrays — MEDIUM
- 7.4 Read latest props/state inside an effect's callback without re-subscribing when those values change — MEDIUM (
useEffectEvent) - 7.5 Subscribe to external mutable state through
useSyncExternalStore, notuseEffect+useState— MEDIUM
- Component Patterns — LOW-MEDIUM
- 8.1 Components receive
refas a normal destructured prop — drop theforwardRefwrapper, drop the drilling — MEDIUM-HIGH - 8.2 Reach for controlled state only when something reads the value on every change — otherwise prefer uncontrolled with
<form action>— LOW-MEDIUM - 8.3 When prop count climbs past ~6 with many optional slots, take
childreninstead and let callers compose — LOW-MEDIUM - 8.4 Tie a stateful child's identity to the entity it edits by setting
key={entity.id}— let React tear down stale state for you — LOW-MEDIUM - 8.5 Use a render prop only when the parent needs to control rendering — for logic reuse, prefer a custom hook — LOW-MEDIUM
- 8.1 Components receive
- Codebase Hygiene — CROSS-CUTTING (multi-file findings; required for whole-repo audits)
- 9.1 Extract duplicated effect/state shapes into a shared hook — HIGH
- 9.2 Consolidate near-duplicate components into one with variants or composition — HIGH
- 9.3 Delete components, hooks, and utilities with zero importers — MEDIUM-HIGH
- 9.4 Demote 'use client' files whose hook usage doesn't require the client — HIGH
- 9.5 Converge on canonical names when the same concept wears different prop names across components — MEDIUM-HIGH
References
- https://react.dev
- https://react.dev/blog/2024/04/25/react-19-upgrade-guide
- https://react.dev/blog/2024/12/05/react-19
- https://react.dev/blog/2025/10/01/react-19-2
- https://react.dev/blog/2025/10/07/react-compiler-1
- https://react.dev/learn/you-might-not-need-an-effect
- https://github.com/facebook/react
Related Skills
- For Next.js 16 App Router, see
nextjs-16-app-routerskill - For client-side form handling, see
react-hook-formskill - For data caching with TanStack Query, see
tanstack-queryskill