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.onLine — useSyncExternalStore(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:
- an object with a value property and a subscribe method for registering change events
- an array holding the current state value and a setter that queues a re-render
- a mutable variable you reassign directly to update the UI
- 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:
- all three calls read the same render's count snapshot, so each queues the identical value
- React throttles state setters to one call per browser event, discarding the extra invocations
- the handler needs an await between calls so React can flush each update
- 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:
- the effect also sets state, which forces its deps to be ignored
- the dependency array must be sorted for React to compare it
- React compares dependencies deeply, and some nested field keeps changing between renders
- 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:
- StrictMode remounts components once in dev to reveal effects lacking proper cleanup
- a race condition in the socket library that production builds happen to optimize away
- the dev server hot-reloading the module, doubling connections on every save
- 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:
- spreading the state subscribes the component to every single field, so any write to any field loops forever
- external stores may only expose primitive values to React
- each call returns a new object, Object.is says the snapshot changed, and React re-renders forever
- 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:
- a full re-render of the component and every one of its children, exactly the same as with any other state setter call
- an error, because setters require a new reference on every call
- a re-render of the children only, skipping the component itself
- 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.