React 19 Best Practices
A condensed summary of the 25 most important best practices drawn from every page in this section.
- Install @types/react 19 Together:
@types/reactand@types/react-dommust be upgraded in the same install to the v19 line; mismatched versions produce confusing type errors (e.g.refmissing fromComponentProps) that look like React bugs. - Use Context Directly as Provider:
<Context.Provider value={…}>is deprecated — render<ThemeContext value={theme}>…</ThemeContext>directly; the old.Providerform still works but logs a warning and is destined for removal. - Only Server Components Can Be async: Mark data-loading components as Server Components and use
awaitdirectly in the body — Client Components cannot be async, cannot useuseState/useEffect, and have no access towindow,document, orlocalStorage. - Pass Server Output Through children: Client Components cannot import Server Components (the import silently becomes client-only), so compose by passing pre-rendered Server Component JSX through
childrenor named JSX props from a Server Component parent. - Serialize More Than JSON: Props crossing the Server→Client boundary can be
Date,Map,Set,FormData, typed arrays, unawaited Promises, and Server Actions — but never regular functions, class instances, or DOM nodes. - Mark Functions, Not Files:
"use server"converts individual async functions into RPC endpoints, so avoid putting it at the top of a component file expecting all exports to become actions — that breaks the component exports instead. - Return a Result Object From Actions: Throwing inside a Server Action crosses the network boundary and can crash the client; return a discriminated result —
return { success: true, data }orreturn { success: false, error: "Invalid" }— and reservethrowfor genuinely unexpected failures caught byerror.tsx. - Watch Inline Action Closure Payloads: Variables captured by inline
"use server"actions are serialized as encrypted hidden form fields on every render, so extract heavy closures into top-level action files when the captured state gets large. - Use useFormStatus in a Child:
useFormStatus()only reflects the nearest ancestor<form>, so calling it in the same component that renders the form always returnspending: false— move the status-reading button into a child of the form. - useActionState Replaces State Entirely: The return value of an action completely overwrites state (no reducer-style merge), so spread the previous state explicitly —
return { ...prevState, error: "Invalid email" }— and remember to import it from"react", not"react-dom". - Prefer Uncontrolled Inputs in Action Forms: Controlled inputs lose user-typed values when an action re-renders the form from error state; uncontrolled inputs (or keeping input values in the returned state) preserve input across validation errors and forms do not auto-reset on success.
- Never Create Promises In Render With use: Creating a new promise inside the component body and passing it to
use()causes an infinite Suspense loop; hoist promise creation to a parent Server Component, a stable cache, or a prop so the reference is stable across renders. - Pair use With Suspense and Error Boundary:
use(promise)requires a Suspense ancestor to show the loading state and an error boundary to catch rejections — without both, the app either throws or silently hangs on pending promises. - Call use Conditionally When You Need To: Unlike hooks,
use()is legal insideif, loops, and early returns —if (shouldLoad) { const data = use(promise) }— take advantage of this for conditional context reads and guarded promise unwraps instead of forcing olduseContext/useEffectshapes. - Call addOptimistic Inside an Action:
addOptimisticsilently does nothing unless invoked inside a form action, Server Action handler, or an explicitstartTransition; wrap event-handler calls:startTransition(() => addOptimistic(newItem))so the optimistic update actually applies. - Keep updateFn Pure: The
updateFnpassed touseOptimisticmust be pure and produce a new value — mutating state or referencing side-effectful scope breaks the automatic rollback on failure. - Surface Errors Alongside Optimistic Rollback: When a failed action rolls back, the UI silently reverts with no indication; pair
useOptimisticwithuseActionStateso errors still render to the user after the revert. - Destructure ref Before Spreading: Function components now accept
refas an ordinary prop, so{...props}accidentally forwardsrefalongside everything else — destructure it explicitly:({ ref, ...props }) => <input ref={ref} {...props} />, especially when the component is a passthrough. - Ref Callbacks Return Cleanup: Ref callbacks may now return a cleanup function (like
useEffect); returning anything else (non-function, non-undefined) triggers a dev warning, so either return a cleanup or return nothing at all. - Give Stylesheets a precedence:
<link rel="stylesheet" precedence="…">opts into React's dedup, ordering, and Suspense-until-loaded — withoutprecedence, the stylesheet is unmanaged and you can hit FOUC or duplicate sheets. - Avoid Multiple title Tags: React hoists
<title>to<head>but only the last-rendered one wins, so scattering titles across components produces order-dependent results; pick a single<title>site (or use the framework's metadata API) and stick with it. - preload Downloads, preinit Executes: Use
preloadwhen you only want the bytes in cache andpreinitwhen you also want scripts to run or stylesheets to apply;preinitruns scripts immediately and will fail if they depend on DOM that has not rendered yet. - Preload Fonts With crossOrigin Anonymous: The Fetch spec requires
crossOrigin: "anonymous"on font preloads even for same-origin requests —preload("/font.woff2", { as: "font", crossOrigin: "anonymous" })— without it the browser downloads the font twice and the preload hint is wasted. - Follow the Rules of React for Compiler: The React Compiler silently skips any component that mutates props/state in render or does side effects during render — no build error, just no memoization — so treat "pure render" as a hard rule if you want the compiler's benefits.
- Run eslint-plugin-react-compiler: The ESLint plugin flags rule violations the compiler would silently skip; combine it with
compilationMode: "annotation"and the"use memo"directive for a gradual rollout, and use"use no memo"to opt specific functions out.