React SME Cookbook
All FAQs

Search Documentation

Search across all documentation pages

best-practicessummaryreact-patterns

React Patterns Best Practices

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

  1. Prefer Named Slots Over One children: Overloading children for multi-region layouts loses placement control; expose named ReactNode props — ({ header, sidebar, footer }: { header: ReactNode; sidebar: ReactNode; footer: ReactNode }) — so consumers can fill each slot independently and you keep TypeScript on their side.
  2. Pick ReactNode, ReactElement, or ComponentType: ReactNode accepts strings/null/arrays (best for slots and children), ReactElement narrows to a single JSX element, and ComponentType<P> is a component you can instantiate — choose intentionally because they type different composition styles.
  3. Build Compound Components on Context: A compound root owns state and publishes it via context so Root.Item, Root.Trigger, and Root.Content work at any JSX depth — guard the consumer hook with a thrown error when used outside the root instead of returning a silent null.
  4. Mark Compound Roots as "use client": Compound components rely on createContext and useState, so the root file must be a Client Component in Next.js — and use two-level context (AccordionContext + per-item ItemContext) to scope identity to each item.
  5. Split State and Actions Contexts: Put state in one context and memoized actions in another so buttons that only dispatch never re-render on state changes; useReducer's dispatch is already React-stable and needs no extra memoization.
  6. Never Inline Object Context Values: Passing a fresh object literal as the context value on every parent render defeats memoization and re-renders every consumer; wrap the value in useMemo keyed on the actual state, or split contexts so updater-only consumers are immune.
  7. Scope Providers Narrowly: Mount a provider on the smallest subtree that actually needs it; a provider at the root blasts re-renders across unrelated pages and makes lazy code-splitting less effective.
  8. Support Both Controlled and Uncontrolled: Library components should detect controlled mode via value !== undefined and fall back to internal state — const [inner, setInner] = useState(defaultValue); const val = value !== undefined ? value : inner — wrap the decision in a reusable useControllableState hook and never switch modes after mount.
  9. Don't Debounce State Updates for Controlled Inputs: Debouncing the onChange that sets state freezes the input because the DOM value lags behind the user; debounce side effects (search, autosave) while always updating controlled state synchronously.
  10. Error Boundaries Need getDerivedStateFromError: Implement getDerivedStateFromError (pure render-phase state derivation) for the fallback switch and componentDidCatch (post-commit) for logging to Sentry; there is no hook equivalent, so use a class or react-error-boundary.
  11. Reset Boundaries via resetKeys + key: Use resetKeys so the boundary auto-recovers when a route param changes, and put a key on the boundary's children so clearing hasError forces a remount — simply resetting state leaves the subtree in its corrupted state.
  12. Know What Error Boundaries Miss: They do not catch errors in event handlers, async code, SSR, or the boundary itself; wrap those in explicit try/catch, and in Next.js rely on the route-level error.tsx convention with error.digest for server errors.
  13. Call HOCs at Module Scope: Calling withAuth(Component) inside another component's body creates a brand-new component type every render and destroys state; apply HOCs once at module scope — const ProtectedPage = withAuth(DashboardPage) — and always set a meaningful displayName.
  14. Forward Refs and Statics Through HOCs: HOCs drop refs and static methods by default, so wrap with React.forwardRef internally and copy statics with hoist-non-react-statics — otherwise consumers lose imperative handles and attached constants.
  15. Profile Before You Memoize: Premature memo/useMemo/useCallback adds comparison overhead that can slow fast components; use React DevTools Profiler to identify real hot spots and remember inline onClick={() => …} or style={{…}} defeats memo via fresh references.
  16. useTransition Owns, useDeferredValue Receives: Use useTransition when you own the state setter and want to mark the update non-urgent; use useDeferredValue to lag a value you receive as a prop, and compare it with the current value to show a subtle stale indicator.
  17. Virtualize Large Lists: Tens of thousands of rows cannot be rescued by memo — reach for react-virtual/react-window or CSS content-visibility: auto, and keep stable keys (IDs, not indices) so rows are not remounted on scroll.
  18. createPortal Keeps the React Tree: Events bubble through the React tree (not the DOM tree) across portals, so clicks inside a modal still hit React ancestors — call stopPropagation when you need a clean break, and trap focus plus save/restore it for real modals.
  19. Guard Portals for SSR: document.body crashes server rendering, so gate the portal with a useEffect-set mounted flag — const [mounted, setMounted] = useState(false); useEffect(() => setMounted(true), []) — or mark the component "use client" in Next.js, and create a dedicated container node instead of piling up empty divs on body.
  20. Beware Inline Render Props: An inline children={(state) => <X />} creates a new function reference on every render and breaks React.memo on the parent; hoist the function or accept that memo will not apply here, and remember hooks cannot be called inside a render-prop function.
  21. Export Prop Collections: Headless components should return spreadable prop objects that bundle event handlers plus ARIA attributes — <button {...triggerProps}>Open</button> <div {...contentProps}>…</div> — consumers cannot forget accessibility wiring because it ships with the collection.
  22. Model State as Discriminated Unions: Replace isLoading/isError/hasData boolean soup with a single status tagged union so impossible combinations are unrepresentable; each state carries only the data valid for that state.
  23. Keep Reducers Pure: Under Strict Mode and concurrent rendering React may call a reducer multiple times, so never fetch, write, or log inside it — and always end with return state for unknown events so unhandled transitions don't produce undefined.
  24. Create Suspense Promises Outside Render: Creating a promise in the component body triggers an infinite suspend loop because use(promise) treats a new reference as a new pending request; hoist promise creation into a parent, a cache, or an event handler.
  25. Pair Suspense With Error Boundary and Transition: A rejected promise without an ancestor error boundary unmounts the whole tree; also use useTransition when replacing Suspense-driven UI so fast fetches don't flash a skeleton, and nest boundaries so secondary content streams in after the critical shell.