Explain the dependency array and cleanup. What's the fetch-in-effect race condition and how do you fix it?
The most misused hook. Dependency correctness and cleanup are senior signals.
useEffect runs after commit; the dependency array decides WHEN it re-runs (empty = once, [x] = when x changes, omitted = every render). The cleanup function runs before the next effect and on unmount. The race: if a prop changes fast, an earlier fetch can resolve AFTER a later one and overwrite fresh data with stale. Fix with a cancelled/ignore flag in cleanup (or an AbortController) so a superseded response is dropped. Deeper points interviewers probe: the dependency array must list EVERY reactive value the effect reads (props, state, and functions/objects defined in render) — omitting one gives you a stale closure that silently reads old values; the honest fixes are to move the value inside the effect, wrap it in useCallback/useMemo, or use a ref. Many effects shouldn't exist at all: don't use an effect to transform data for rendering (compute during render), to reset state on prop change (use a key), or to handle a user event (do it in the handler). The mental model react.dev pushes is 'synchronize with an external system' — a subscription, the DOM, a network resource — and every synchronization needs its teardown, which is what cleanup is for.
Data fetching tied to props, subscriptions, timers, event listeners — anything with setup that needs teardown.
useEffect(() => {
let ignore = false;
fetch(`/api/user/${id}`)
.then(r => r.json())
.then(data => { if (!ignore) setUser(data); }); // drop stale response
return () => { ignore = true; }; // cleanup on id change / unmount
}, [id]);