React SME Cookbook
All FAQs

Search Documentation

Search across all documentation pages

best-practicessummaryreact-19

React 19 Best Practices

A condensed summary of the 25 most important best practices drawn from every page in this section.

  1. Install @types/react 19 Together: @types/react and @types/react-dom must be upgraded in the same install to the v19 line; mismatched versions produce confusing type errors (e.g. ref missing from ComponentProps) that look like React bugs.
  2. Use Context Directly as Provider: <Context.Provider value={…}> is deprecated — render <ThemeContext value={theme}>…</ThemeContext> directly; the old .Provider form still works but logs a warning and is destined for removal.
  3. Only Server Components Can Be async: Mark data-loading components as Server Components and use await directly in the body — Client Components cannot be async, cannot use useState/useEffect, and have no access to window, document, or localStorage.
  4. 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 children or named JSX props from a Server Component parent.
  5. 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.
  6. 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.
  7. 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 } or return { success: false, error: "Invalid" } — and reserve throw for genuinely unexpected failures caught by error.tsx.
  8. 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.
  9. Use useFormStatus in a Child: useFormStatus() only reflects the nearest ancestor <form>, so calling it in the same component that renders the form always returns pending: false — move the status-reading button into a child of the form.
  10. 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".
  11. 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.
  12. 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.
  13. 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.
  14. Call use Conditionally When You Need To: Unlike hooks, use() is legal inside if, 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 old useContext/useEffect shapes.
  15. Call addOptimistic Inside an Action: addOptimistic silently does nothing unless invoked inside a form action, Server Action handler, or an explicit startTransition; wrap event-handler calls: startTransition(() => addOptimistic(newItem)) so the optimistic update actually applies.
  16. Keep updateFn Pure: The updateFn passed to useOptimistic must be pure and produce a new value — mutating state or referencing side-effectful scope breaks the automatic rollback on failure.
  17. Surface Errors Alongside Optimistic Rollback: When a failed action rolls back, the UI silently reverts with no indication; pair useOptimistic with useActionState so errors still render to the user after the revert.
  18. Destructure ref Before Spreading: Function components now accept ref as an ordinary prop, so {...props} accidentally forwards ref alongside everything else — destructure it explicitly: ({ ref, ...props }) => <input ref={ref} {...props} />, especially when the component is a passthrough.
  19. 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.
  20. Give Stylesheets a precedence: <link rel="stylesheet" precedence="…"> opts into React's dedup, ordering, and Suspense-until-loaded — without precedence, the stylesheet is unmanaged and you can hit FOUC or duplicate sheets.
  21. 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.
  22. preload Downloads, preinit Executes: Use preload when you only want the bytes in cache and preinit when you also want scripts to run or stylesheets to apply; preinit runs scripts immediately and will fail if they depend on DOM that has not rendered yet.
  23. 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.
  24. 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.
  25. 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.