Quiz - Effects, context, and concurrent hooks
Your effect runs twice in dev. A teammate “fixes” it with a useRef(false) guard that skips the second run, and the doubling stops. Why is this the wrong move?
The guard hides the symptom but leaves the missing cleanup in place — and React 19 genuinely mounts, unmounts, and remounts components in production, where the leak it was masking still bites.
useRef(false) re-initializes to false on every render, so the guard never actually holds and the second run still fires.
Strict Mode double-invokes the ref guard too, so the two extra runs cancel out and leave the effect running an odd number of times.
In a chat component, roomId changes from "general" to "random". In what order does React run the effect’s cleanup and setup, and why?
Cleanup first (disconnect general), then setup (connect random) — cleanup runs before every re-sync, not only on unmount.
Setup first (connect random), then cleanup (disconnect general), because the new connection must exist before the old one is dropped.
Only setup runs; cleanup is reserved for when the component unmounts entirely.
general socket open.Two engineers want a chat socket to reconnect only when roomId changes, while still calling the freshest onMessage and currentUser on each message. One wraps the handler in useCallback([onMessage, currentUser]); the other moves it into a useEffectEvent. Why does only the Effect Event actually work?
useCallback returns a stable function over a fixed snapshot — a real change to currentUser still mints a new identity and reconnects the socket. useEffectEvent reads the latest values yet stays out of the deps entirely, so [roomId] is the only thing that re-syncs.
useCallback can’t be given a dependency array inside a component, so it captures a stale currentUser and never updates.
Both work identically; useEffectEvent is just newer syntax for the same useCallback behavior.
useCallback only stabilizes identity — when its deps legitimately change, the new function identity re-runs the effect, the exact reconnect you were killing. An Effect Event is an unstable function over the latest values that is excluded from deps by design, so the genuinely-reactive roomId is the lone dependency.You run the audit’s two-question code review on a useEffect: “What external system does it synchronize with?” and “What does its cleanup tear down?” Both answers come back blank. What does that tell you?
It’s almost certainly one of the catalog anti-patterns in disguise — a derived value, misplaced handler logic, or a fetch — and should be reshaped, not kept.
The effect is correctly minimal; an empty cleanup is the sign of a well-scoped effect with no side effects to undo.
The effect just needs an AbortController added to its cleanup to become a legitimate synchronization.
AbortController only race-proofs an effect that shouldn’t exist.A reducer-backed NotificationsContext exposes { state, dispatch } as one value. A “Clear all” button reads only dispatch, yet it re-renders on every toast that comes and goes. What is the canonical fix?
Split into two contexts — a state context and a dispatch context — so dispatch-only consumers subscribe to dispatch, whose reference is stable for the component’s life and never changes.
Wrap the { state, dispatch } object in useMemo so its reference stays stable across notification changes.
Read dispatch with use(NotificationsContext) instead of useContext, which subscribes to only the dispatch field.
dispatch is reference-stable, so a dispatch-only context never updates and its consumers stop re-rendering entirely. useMemo can’t help — the value genuinely changes on every notification, so its reference should change. And use(Context) only relaxes where the call can sit; it does not narrow the subscription to one field.“Marking the slow list update as a transition will make filtering 5,000 rows faster.” Is this right?
No — transitions are priority markers, not speed boosts. The filter render costs exactly what it did; it just no longer blocks the keystroke, rendering in an interruptible background lane instead.
Yes — startTransition batches the work and skips intermediate renders, so the final filter runs faster.
Yes — wrapping the update in a transition debounces it, so the expensive filter runs fewer times overall.
A Client Component reads its data with use(fetch('/api/activity').then(r => r.json())) written directly in the render body. It spins forever and the Network tab fires the same request on a loop. Why?
The render body creates a brand-new promise on every render. use() tracks promises by reference, sees a new resource each time, suspends, resolves, re-renders, and creates another promise — an endless loop.
use() may only read a promise that originates in a Server Component; calling it in a Client Component always re-suspends.
The promise needs to be wrapped in a <Suspense> boundary, and without one use() retries the fetch indefinitely.
use() identifies the resource by Object.is reference, so the promise must be referentially stable across renders — created once in a Server Component or held in a stable reference, not minted inside the render body. use() works fine in Client Components, and a missing Suspense boundary suspends the tree, it doesn’t cause a refetch loop.A junior reads that use() can be called inside an if and concludes “hooks can be conditional now,” so they wrap a useState in a condition. Why does this break, when the conditional use() did not?
useState claims a numbered slot by call order, so skipping it on some renders drifts the slot count and misaligns every hook below it. use() claims no slot — it’s tracked by reference or tree position — so moving it lines up against nothing.
useState is fine inside an if too; the lint flags it only as a style warning, not a real bug.
useState must be conditional-free because it triggers re-renders, whereas use() never does.
use() alone and generalizes to nothing. Regular hooks rely on a stable call order so React’s positional slot pointer finds the right stored value; a conditional useState makes the count drift and corrupts state with no error. use() needs no slot, so the rule it would break simply doesn’t apply.Quiz complete
Score by topic