The hooks, and which note goes deep on each
A map, not a tutorial. Each hook gets what it is for, the trap that comes up in interviews, and where the depth lives. The deep notes are the ones to read; this page is for placing a hook you have half-forgotten.
Two facts frame everything below. Hooks are stored by call order, which is
why the Rules of Hooks exist — see
Stale Closures and the Rules of Hooks.
And the React Compiler is the baseline for new projects, which turned most
useMemo/useCallback advice into history — see
React Compiler.
State and effects
| Hook | For | The trap |
|---|---|---|
useState |
local state | updates are batched; read stale |
useReducer |
state with transitions | reducer must be pure |
useEffect |
sync with something external | missing cleanup, wrong deps |
useLayoutEffect |
measure before paint | blocks paint; rarely right |
useRef |
a box that survives renders | changing it does not re-render |
useState — the update is queued, not applied, so setCount(count + 1)
twice in one handler adds one. Use the functional form when the next value
depends on the previous:
setCount((c) => c + 1); // correct twice in a rowuseReducer earns its place when several pieces of state change together
and the transitions have names — { type: "submit" } reads better than four
setX calls, and the reducer is testable without React.
useEffect is for synchronising with something outside React: a
subscription, a timer, a DOM API. It is not for deriving state from props —
compute that during render. Deps, cleanup and the fetch-in-effect trap are in
useEffect Deep — Cleanup, StrictMode Double-Invoke, Dep-Array Bugs, Effect Events.
useRef holds a mutable value across renders without causing one. DOM
refs are the obvious use; the less obvious one is holding the latest callback
or an interval id. Detail in useRef Hook in React.
Memoisation, after the compiler
useMemo, useCallback and React.memo still exist, and the compiler now
inserts the equivalent automatically in most cases. Reach for them by hand
only where the compiler cannot see the dependency — an expensive computation
whose inputs it cannot prove stable — and measure first.
Memoization Heuristics — When useMemo/useCallback/memo Help vs Hurt
has the heuristics; React.memo has the mechanism.
Context and external state
useContext reads a value provided above. Every consumer re-renders when
the value changes, so a context holding a large, frequently changing object is
the classic performance problem — split contexts, or move server state to a
data library. See Context Performance Traps.
useSyncExternalStore is how a store outside React (Zustand, Redux, a
browser API) integrates without tearing under concurrent rendering. You rarely
call it; the libraries do. Knowing why it exists is the interview point.
Concurrent rendering
useTransition marks an update as non-urgent so React can keep the UI
responsive — typing in a filter while a large list re-renders behind it.
useDeferredValue is the same idea for a value rather than an update.
Both are in Concurrent Rendering — useTransition, useDeferredValue, How Priorities Work.
useId generates ids stable across server and client, for aria-* and
htmlFor. Never use it for list keys.
React 19
Four additions that change how forms and data loading are written; the deep treatment is React 19 — Actions, useActionState, useOptimistic, use(), ref as Prop.
use()reads a promise or a context, and unlike other hooks may be called conditionally. It is what makes Suspense-for-data ergonomic.useActionStatewraps a form action: state, the action to pass to<form action>, and a pending flag, in one call.useOptimisticshows the expected result while the action is in flight and reverts on failure, without a cache layer.useFormStatus(fromreact-dom) lets a submit button inside a form know the form is pending without prop drilling.
Writing a custom hook
A custom hook is a function that calls hooks. That is the entire definition, and the discipline is that it should package one concern:
function useDebounced<T>(value: T, ms = 300): T {
const [debounced, setDebounced] = useState(value);
useEffect(() => {
const id = setTimeout(() => setDebounced(value), ms);
return () => clearTimeout(id); // the cleanup is the point
}, [value, ms]);
return debounced;
}The interview follow-up is always the cleanup: without clearTimeout, every
keystroke schedules a timer and the last one wins only by accident.
Three that are worth having in your head because they come up: a debounce as
above, usePrevious (a ref updated in an effect), and a fetch hook — which is
the one to not write, because it is what TanStack Query is; see
TanStack Query and server state.
Where it breaks in production
- A hook inside a condition or loop. Call order changes between renders, and React reads the wrong slot. The lint rule exists for this.
useEffectwith no cleanup on anything that subscribes. Leaks and double-firing under Strict Mode’s mount-unmount-mount.- State derived in an effect.
useEffect(() => setTotal(a + b), [a, b])is a render behind and a re-render too many. Compute it inline. useLayoutEffecton the server. It warns and does nothing; use it only for DOM measurement that must precede paint.useIdas a key. It is stable across hydration, not across list reorders. Keys come from the data.
Related
Interview angle 4
- “Why can’t hooks be called conditionally?” - React stores hook state by
call order, so a skipped call shifts every later hook into the wrong slot.
Follow-up: which hook is the exception?
use(), by design. - “
useMemoeverywhere?” - not since the compiler. It memoises what it can prove stable; hand-written memoisation is for the cases it cannot see, and only after measuring. ReflexiveuseCallbackis now a signal of outdated habits. - “When would you write a custom hook?” - when one concern - a debounce, a subscription, a media query - recurs across components. Not for fetching: that is a cache problem and a library already solves it.
- “
useEffector compute during render?” - if the value derives from props or state, compute it. An effect is for synchronising with something outside React, and using it to set state is a render behind every time.