React SME Cookbook
All FAQs

Search Documentation

Search across all documentation pages

best-practicessummaryreact-fundamentals

React Fundamentals Best Practices

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

  1. Never Define Components Inside Components: A component declared inside another component gets a fresh identity on every parent render, so React unmounts and remounts it each time — destroying local state and effects; always hoist components to module scope: function Parent() { function Child() {} /* bad — Child remounts every render */ }.
  2. Treat Props as Immutable: Mutating a prop (e.g., props.items.push(x)) quietly mutates the parent's data and breaks React's rendering model; copy into new arrays or objects before modifying and let the parent own the state.
  3. Extend Native Props With ComponentPropsWithoutRef: Type wrapper components with React.ComponentPropsWithoutRef<"button"> so every native attribute stays in sync with the DOM API; re-declaring props manually drifts the moment the DOM spec changes.
  4. Prefer interface for Props: interface gives better error messages and supports declaration merging, so use it for component prop shapes; reserve type for unions, mapped types, and other transformations that interfaces cannot express.
  5. Guard Against 0 in && Conditionals: {count && <Badge />} renders the literal string "0" when count is 0 because 0 is falsy but still renderable; use count > 0 && <Badge /> or a ternary to avoid the infamous "0" bug.
  6. Discriminated Unions for Status: Model state as { status: "success"; data } | { status: "error"; error } so TypeScript narrows inside each branch; this eliminates optional-chaining noise and makes unreachable branches obvious.
  7. Switching Element Types Resets State: Changing a component type at the same tree position (e.g., <Spinner /><DataTable />, or <input><textarea>) unmounts the old subtree and wipes its state; keep the same type and toggle props when you want state to persist.
  8. Pass Handlers, Not Invocations: onClick={handleClick} attaches the function; onClick={handleClick()} calls it during render and wires the return value as the handler, which is almost never what you want — wrap in an arrow when you need to inject arguments: onClick={() => handleClick(id)}.
  9. onChange Fires Every Keystroke: React's onChange maps to the native input event, not native change, so it fires on every keystroke instead of on blur — use onBlur if you actually want "commit on blur" semantics.
  10. Use currentTarget, Not target: e.target is the element the event originated on (which may be a child), while e.currentTarget is the element the handler is attached to; read attributes you control from currentTarget to avoid surprises when clicking nested elements.
  11. Escape to addEventListener When Needed: JSX handler props do not support { passive: false } or window/document-level events; for Escape-key listeners, scroll monitors, or touchmove needing preventDefault, use useEffect + addEventListener on a ref and return a cleanup.
  12. Controlled Inputs Need value + onChange: Setting value without a matching onChange makes the input read-only because React pins the DOM value to state; either pair the two — <input value={name} onChange={e => setName(e.target.value)} /> — or switch to defaultValue for an uncontrolled input.
  13. Checkboxes and Radios Use checked: Checkbox and radio inputs consume checked/defaultChecked, not value/defaultValue; mixing them up silently does the wrong thing since value on a checkbox is the submitted token, not the checked state: <input type="checkbox" checked={on} onChange={e => setOn(e.target.checked)} />.
  14. Call useFormStatus in a Child: useFormStatus() only returns accurate pending info when called from inside a descendant of the <form>; calling it in the same component that renders the form returns stale defaults with no error, so extract the submit button into its own component.
  15. Parse input.value Strings: e.target.value is always a string — even for <input type="number"> — so wrap it with Number(...) or parseInt(...) before using it in math or storing typed state: onChange={e => setAge(Number(e.target.value))}.
  16. Use className, Not class: JSX compiles to React.createElement, so HTML's class attribute is className, for is htmlFor, and styles take a camelCase object; using the HTML names compiles but warns and can silently drop styling in strict environments.
  17. Objects Are Not Valid Children: Rendering {user} throws "Objects are not valid as a React child"; render a specific field like {user.name} or JSON.stringify(user) for debug output, and remember that 0 renders as text but null/false/undefined render nothing.
  18. ReactNode for children, JSX.Element for Returns: Type children as React.ReactNode (which covers JSX, strings, numbers, arrays, null) and reserve React.JSX.Element for return types that always return a single JSX element.
  19. Keys Unique Among Siblings: A list's key must be unique among its siblings (not globally), and it must be stable across renders — use item.id, not Math.random() or Date.now(), or React will remount every row and destroy its state.
  20. Avoid Index Keys on Dynamic Lists: Using index as key on lists that reorder, filter, or insert causes React to reuse the wrong DOM nodes, producing stale input values and broken focus; index keys are only safe for truly static lists.
  21. Use key to Force Remount: Changing the key on a component is the canonical way to reset all of its internal state — <PlayerProfile key={currentPlayerId} /> deliberately remounts when the player changes, which is a feature, not a hack.
  22. Short Fragments Can't Take key: The <>...</> shorthand does not accept a key prop; when mapping fragments, import Fragment and use <Fragment key={...}>...</Fragment> to avoid duplicate-key warnings or unkeyed renders.
  23. ref.current Doesn't Trigger Renders: Updating ref.current does not re-render the component — that is the point; reach for useRef when the UI does not depend on the value (timer IDs, observers, latest-value cache) and useState when it does.
  24. ref Is a Regular Prop in React 19: forwardRef is deprecated; just type ref as an ordinary prop with React.Ref<HTMLInputElement> and destructure it alongside the others — and remember {...props} now spreads ref too, so pull it off explicitly when passing through.
  25. Return Cleanup From Ref Callbacks: React 19 ref callbacks can return a cleanup function like useEffect, so wire observers as ref={node => { if (!node) return; const obs = new ResizeObserver(...); obs.observe(node); return () => obs.disconnect(); }} instead of the old null-check dance.