React · cheat sheet

React Hooks

Every built-in React hook as of React 19.2, plus the use API: one example each, the rules of hooks, effect timing and the gotchas interviewers ask about.

Quiz me on React

Every built-in hook in React 19.2 (and the use API), each with one example and the detail people forget.

Rules of Hooks

  • Call hooks at the top level of a component or custom hook: never in conditions, loops, nested functions or after an early return.
  • React matches hooks to their state by call order, which is why the order must be identical on every render.
  • A custom hook is a use-prefixed function that calls other hooks; each call gets its own state.
  • use is the exception: it may be called inside if and loops, but still only in components and hooks.
  • Enable eslint-plugin-react-hooks: rules-of-hooks and exhaustive-deps catch most bugs.

State

  • useState: const [count, setCount] = useState(0). Use the updater setCount((c) => c + 1) when the next value depends on the previous one. useState(() => init()) runs init only once.
  • useReducer: the update logic lives in a pure function. A third argument, init, computes the initial state lazily.
function reducer(todos, action) {
  switch (action.type) {
    case 'added': return [...todos, action.todo];
    case 'removed': return todos.filter((t) => t.id !== action.id);
    default: throw new Error(`Unknown action: ${action.type}`);
  }
}
const [todos, dispatch] = useReducer(reducer, []);
dispatch({ type: 'added', todo: { id: 1, text: 'Ship it' } });
JavaScript

Context & refs

  • useContext: const theme = useContext(ThemeContext) reads the nearest provider above. Consumers re-render when the value changes (Object.is). React 19 lets you render <ThemeContext value="dark"> instead of <ThemeContext.Provider>.
  • useRef: const inputRef = useRef(null), then <input ref={inputRef} /> and inputRef.current.focus(). Changing .current never re-renders; don’t read or write it during render.
  • useImperativeHandle: exposes a custom handle instead of the DOM node. In React 19, ref is a regular prop, so forwardRef is no longer needed.
function Field({ ref }) {
  const inputRef = useRef(null);
  useImperativeHandle(ref, () => ({
    focus: () => inputRef.current.focus(),
    clear: () => { inputRef.current.value = ''; },
  }), []);
  return <input ref={inputRef} />;
}
JavaScript

Effects

  • useEffect: syncs with an external system. The cleanup runs before the next setup and on unmount.
useEffect(() => {
  const connection = createConnection(roomId);
  connection.connect();
  return () => connection.disconnect();
}, [roomId]); // no array: every render; []: after mount only
JavaScript
  • useLayoutEffect: same signature, but runs after DOM changes and before paint, so you can measure without flicker: useLayoutEffect(() => setHeight(ref.current.offsetHeight), []). It blocks painting; prefer useEffect.
  • useInsertionEffect: for CSS-in-JS libraries, fires before any layout effect: useInsertionEffect(() => injectStyle(rule), [rule]). It cannot update state, and refs are not attached yet.
  • useEffectEvent (stable since 19.2): reads the latest props and state from inside an effect without making them dependencies. Call it only from effects; never pass it on or list it in deps.
const onConnected = useEffectEvent(() => showToast('Connected', theme));
useEffect(() => {
  const connection = createConnection(roomId);
  connection.on('connected', onConnected);
  connection.connect();
  return () => connection.disconnect();
}, [roomId]); // changing theme does not reconnect
JavaScript
Hook Runs
useInsertionEffect Before layout effects
useLayoutEffect After DOM updates, before paint
useEffect Usually after paint

In Strict Mode (development only), React runs setup, cleanup, then setup again on mount to expose missing cleanups.

Performance

  • useMemo: const visible = useMemo(() => filterTodos(todos, tab), [todos, tab]) caches a calculation between renders. Treat it as an optimization, not a guarantee.
  • useCallback: const onSubmit = useCallback((data) => post(id, data), [id]) caches a function; it equals useMemo(() => fn, deps).
  • Both only help when the value goes to a memo child or into a dependency array. The React Compiler can add this memoization for you.

Transitions & deferred values

  • useTransition: const [isPending, startTransition] = useTransition(), then startTransition(() => setTab('posts')). Marks an update as non-urgent. Since React 19 the function may be async (an Action). Not for controlled text inputs.
  • useDeferredValue: const deferred = useDeferredValue(query); render the slow list with deferred, and query !== deferred means stale results are showing. React 19 adds an optional initialValue. Pass primitives or objects created outside render.

Actions & forms

  • useActionState: returns [state, formAction, isPending]. The action receives (previousState, payload); pass formAction to a form’s action prop, or call it inside startTransition.
async function addToCart(prevState, formData) {
  const res = await api.add(formData.get('id'));
  return res.ok ? { error: null } : { error: 'Could not add item' };
}
const [state, formAction, isPending] = useActionState(addToCart, { error: null });
// <form action={formAction}> ... {state.error}
JavaScript
  • useOptimistic: shows a temporary value while an Action is pending, then falls back to the real one. Call the setter inside an Action.
const [optimisticLikes, setOptimisticLikes] = useOptimistic(likes);
const like = () => startTransition(async () => {
  setOptimisticLikes(likes + 1); // visible immediately
  setLikes(await api.like());    // the real value when the Action ends
});
JavaScript
  • useFormStatus (from react-dom): const { pending } = useFormStatus() inside a SubmitButton gives the parent form’s status. It must be rendered inside the form, not in the component that renders it.

Utilities & use

  • useId: const id = useId(), then <label htmlFor={id}> and <input id={id} />. Stable across server and client rendering; not for list keys.
  • useSyncExternalStore: subscribes to a store outside React. Declare subscribe outside the component, and have getSnapshot return the same value while nothing changed, or React re-renders forever.
function subscribe(onChange) {
  window.addEventListener('online', onChange);
  window.addEventListener('offline', onChange);
  return () => {
    window.removeEventListener('online', onChange);
    window.removeEventListener('offline', onChange);
  };
}
const isOnline = useSyncExternalStore(subscribe, () => navigator.onLine, () => true);
JavaScript
  • useDebugValue: useDebugValue(isOnline ? 'Online' : 'Offline') inside a custom hook labels it in React DevTools.
  • use: const comments = use(commentsPromise) suspends until the promise resolves (show a Suspense fallback; errors reach an Error Boundary, not try/catch). Pass a cached or server-created promise; one created during render is new on every render. use(ThemeContext) works like useContext, conditionally.

Gotchas

  • Stale closures: callbacks and effects see the values of the render that created them. Fix the deps, use the updater form or useEffectEvent.
  • State is a snapshot: calling setCount(count + 1) twice in one handler adds 1; setCount((c) => c + 1) twice adds 2.
  • Unstable dependencies: an object or function created during render is new every time, so the effect re-runs on every render. Move it inside the effect or memoize it.
  • Derive, don’t sync: if a value can be computed from props or state during render, don’t copy it into state with an effect.
  • Keys reset state: changing a component’s key remounts it with fresh state.
esc