Hooks & Lifecycle

Roughly 25% of a typical loop — and 25% of the mock exam here.

useState and useReducer

Interviewers open with state because the traps are cheap to set. Calling the setter with a plain value uses whatever the closure captured, so setCount(count + 1) twice in one handler adds one, not two. The functional updater setCount(c => c + 1) reads the pending state instead — that is the rule that resolves every “why did it only increment once” question. Lazy initialization, useState(() => expensive()), runs the initializer once on mount; passing expensive() directly runs it on every render even though the result is discarded.

useReducer is the answer when the next state depends on structured transitions: the reducer is pure and testable without rendering, and dispatch has a stable identity, so it drops out of dependency arrays.

useEffect: dependencies, cleanup, timing

Dependencies are compared with Object.is:

  • No array: the effect runs after every commit.
  • Empty array: once after mount (plus a deliberate setup, cleanup, setup cycle in dev Strict Mode — a bug surfaced there is a real cleanup bug).
  • Listed deps: after mount, then after any commit where a dep changed. Cleanup runs before each re-run with the old values, and once more on unmount.

Timing is the discriminator interviewers want. useEffect fires after the browser paints; useLayoutEffect fires after DOM mutation but before paint, blocking it. Default to useEffect; reach for useLayoutEffect only to measure or position DOM and kill visible flicker. For data that lives outside React — a store singleton, navigator.onLineuseSyncExternalStore(subscribe, getSnapshot) replaces the effect-plus-setState pattern: it avoids tearing during concurrent renders, and getSnapshot must return a cached, immutable snapshot, not a fresh object per call.

useCallback, useMemo, and stale closures

Both cache across renders by dependency comparison; useCallback(fn, deps) is exactly useMemo(() => fn, deps). Their real job is referential equality: a child wrapped in memo re-renders anyway if you hand it a fresh object or callback each render. They are performance tools only — correctness must never depend on them — and react.dev notes the React Compiler now auto-memoizes, shrinking how often you write either by hand.

The stale closure is the signature trap: an effect with [] captures the first render’s values forever.

useEffect(() => {
  const id = setInterval(() => setCount(count + 1), 1000); // always 0 + 1
  return () => clearInterval(id);
}, []); // count is stale — use setCount(c => c + 1) or list the dep

useRef, useContext, useId

Mutating ref.current never triggers a re-render, which makes refs the home for values that must survive renders without driving them: timer ids, previous props, DOM nodes. Never read or write a ref during render. For DOM access, pass the ref to a JSX ref attribute and React fills in the node after commit. Since React 19, function components receive ref as an ordinary prop — forwardRef is no longer needed and is slated for deprecation — and a ref callback may return a cleanup function that React calls on unmount.

useContext(SomeContext) reads the nearest provider’s value; every consumer re-renders when that value changes identity, so memoize the object you pass down. React 19 also lets you render SomeContext itself as the provider. useId generates hydration-safe unique ids for accessibility wiring like aria-describedby — never for list keys, which must come from your data.

Rules of hooks and custom hooks

Hooks are called unconditionally, at the top level of a component or custom hook. The why is the interview answer: React stores hook state by call order, so a hook behind an if shifts every later slot and corrupts state. Custom hooks are plain functions named useX that call other hooks — they share stateful logic, not state. Two components calling the same custom hook get fully independent state, a distinction interviewers probe deliberately.

Sample questions

6 of the 50 questions this domain carries in practice mode — expand one to check yourself before drilling.

1. Calling useState(0) in a function component returns:
  1. an object with a value property and a subscribe method for registering change events
  2. an array holding the current state value and a setter that queues a re-render
  3. a mutable variable you reassign directly to update the UI
  4. a Proxy that re-renders the component whenever any property is read

Answer: B. The returned pair is conventionally destructured; the setter has a stable identity across renders, so it is safe to omit from dependency arrays.

2. A handler calls setCount(count + 1) three times, yet count advances by only 1 per click. The cause is:
  1. all three calls read the same render's count snapshot, so each queues the identical value
  2. React throttles state setters to one call per browser event, discarding the extra invocations
  3. the handler needs an await between calls so React can flush each update
  4. the component is missing a key, so React cannot track the queued updates

Answer: A. State variables are per-render snapshots; passing an updater function like c => c + 1 makes each queued update read the previous result instead.

3. An effect with dependency array [options] fires on every render even though options looks identical each time. Most likely:
  1. the effect also sets state, which forces its deps to be ignored
  2. the dependency array must be sorted for React to compare it
  3. React compares dependencies deeply, and some nested field keeps changing between renders
  4. options is a new object each render, and Object.is sees each one as different

Answer: D. Dependency comparison is shallow Object.is per element; hoist the object, memoize it, or depend on its primitive fields instead.

4. In development a chat component connects twice to the socket server on mount; in production it connects once. This indicates:
  1. StrictMode remounts components once in dev to reveal effects lacking proper cleanup
  2. a race condition in the socket library that production builds happen to optimize away
  3. the dev server hot-reloading the module, doubling connections on every save
  4. that useEffect always runs twice unless it is wrapped in useMemo

Answer: A. The intended fix is a cleanup that disconnects, making mount-unmount-mount harmless; disabling StrictMode merely hides the missing symmetry.

5. A useSyncExternalStore hook whose getSnapshot returns { ...store.state } sends the component into an infinite re-render loop. Because:
  1. spreading the state subscribes the component to every single field, so any write to any field loops forever
  2. external stores may only expose primitive values to React
  3. each call returns a new object, Object.is says the snapshot changed, and React re-renders forever
  4. getSnapshot runs during render, where object allocation is forbidden

Answer: C. getSnapshot must return a cached or immutable value that stays identical until the store truly changes; React compares successive snapshots by reference.

6. A click handler calls setItems(items), passing back the exact same array reference already in state. The result is:
  1. a full re-render of the component and every one of its children, exactly the same as with any other state setter call
  2. an error, because setters require a new reference on every call
  3. a re-render of the children only, skipping the component itself
  4. no update; React bails out via Object.is, though it may still run this component once before skipping children

Answer: D. The bailout comparison is Object.is on the stored value, which is also why mutating an array in place and setting it back shows nothing.

Drill this domain in practice mode →

Independent community study resource — not affiliated with or endorsed by Meta Platforms, Inc. React is a trademark of Meta Platforms, Inc. All questions and study notes are original, written from the official React documentation. Everything runs in your browser; nothing you answer is stored or transmitted.