GAP·MAP
← all posts
July 11, 2026questions

"React interview questions: re-renders, hooks, state"

Most React rounds circle the same loop: something rendered when you didn't expect it, a hook captured a value from the wrong render, or state landed in the wrong place. Interviewers rarely want the API surface. They want the mechanism, and they plant bugs that only the mechanism explains. This list comes from the coverage maps behind our React modules and what shows up in real loops.

At a glance, what each area tends to ask:

AreaTypical questionThe lesson
re-renders"This memo'd child still re-renders every keystroke, why?"React re-renders
hooks"The interval counter is stuck at 1, fix it"React hooks
state"Where do the filters of a product list live?"State management
SSR"Hydration failed on a header with a timestamp, walk me through it"SSR & hydration
patterns"This input ignores typing, diagnose it"React patterns

Why did this re-render?

The first filter is whether you know what triggers a render at all. A render is React calling your component function; it is not a DOM update. It happens on mount, or when a state setter runs on the component or one of its ancestors. Props changing on their own trigger nothing. The parent re-rendering is what passes new props down.

Type in an input, delete a row above it, the text jumps

Trap index-as-key. State and DOM stay glued to the list position, not to the data, so deleting the first row shifts every value up one.

todos.map((todo, i) => <Row key={i} todo={todo} />)     // WRONG: state glued to position
todos.map((todo) => <Row key={todo.id} todo={todo} />)  // RIGHT: identity follows the data

Index keys are safe only for a static list that never reorders, filters, or inserts. The moment it does any of those, position and identity drift apart.

The memo'd child that re-renders anyway

memo skips a render only when every prop passes Object.is. An inline object, array, or arrow function is a fresh reference every render, so the comparison fails every time.

<Profile person={{ name: 'Ann' }} onSave={() => save()} />  // WRONG: two new refs each render

Follow-up "So do we wrap everything in useCallback?" No. Memoization is a bet: comparison and memory cost against render cost. It buys nothing without a memo boundary downstream, and colocating state or passing JSX as children usually removes the need before you reach for it.

Study React re-renders

Stale closures in hooks

This is the single most reliable hook question, because it separates people who recite "add it to the deps" from people who can explain closures. Each render creates fresh bindings, and a callback captures the snapshot from the render it was created in. When the deps lie, the callback keeps reading a stale snapshot.

The interval counter stuck at 1

useEffect(() => {
  const id = setInterval(() => setCount(count + 1), 1000); // always reads 0 + 1
  return () => clearInterval(id);
}, []); // lint suppressed, closure frozen on render #1

The empty dep array runs setup once, so the interval closes over count from the first render forever. The fix is not to add count to the deps and restart the timer every tick. Use the updater form, setCount(c => c + 1), which reads the queued value instead of the captured one, and keep the array empty.

Red flags suppressing exhaustive-deps to silence the warning, or claiming this is a React quirk rather than ordinary JavaScript closure capture. The dependency array is derived from the code you wrote, never chosen to make the warning stop.

Cleanup timing trips people up here too. Cleanup runs before every re-run and on unmount, not only on unmount, which is why a fetch effect needs a race guard: set let ignore = false, flip it in cleanup, and drop the response if it comes back after the input changed. Predicting the order of that setup, cleanup, and the microtask that resolves the fetch draws directly on the event loop.

Study React hooks

Where does this state belong?

The classify-and-justify question is a senior staple: "here is an app that fetches everything into a global store with per-slice loading flags, critique and redesign it." The answer is to sort state by kind before choosing a tool. Server data is a cache of something you do not own, so it needs staleness and invalidation, which a plain store does not provide. That is the case for a query library over hand-rolled useEffect fetching.

Where do the product-list filters live?

In the URL. A filter in localStorage is not shareable and breaks the back button; a filter in a store is not linkable. The URL is the one place that survives reload, share, and history navigation.

The search box that re-renders half the app

Trap Context is dependency injection for a value, not a state manager with selective subscription. Every consumer re-renders when the Context value changes, so putting fast-changing input state in a top-level provider repaints everything that reads it. Colocate the state, or move to a store whose consumers subscribe through selectors with equality checks.

Follow-up "Design an optimistic like button, what can go wrong?" Without cancelling in-flight refetches, a background response overwrites your optimistic value; without a snapshot, there is nothing to roll back to. That rollback-and-reconcile design is its own topic in frontend data flow.

Study State management

SSR and hydration

Hydration is React rendering over the server's existing DOM and attaching event handlers, not re-downloading the page. The invariant is that the first client render must produce the same tree the server sent. Break it and React throws the tree away and re-renders on the client.

"Hydration failed" on a header with a timestamp

Trap Date.now(), Math.random(), or a typeof window !== 'undefined' branch in render. Each produces different output on server and client, which is the documented cause of a mismatch, not the safe pattern for it. Browser-only reads belong in an effect, which never runs on the server.

Follow-up "What does a Server Component give you that SSR doesn't?" They are different axes. SSR produces first-paint HTML; Server Components remove code from the client bundle and never hydrate. Client components still SSR and still hydrate. A candidate who says "RSC replaces SSR" has the model wrong.

SSR makes a page visible sooner, but interactivity still waits for the bundle to download and hydration to finish. Closing that gap is a measurement problem shared with loading performance and Core Web Vitals.

Study SSR & hydration

Controlled components and patterns

The input that ignores typing

Trap a value prop with no onChange, or a value that starts as undefined and later becomes a string. The first freezes the input; the second flips it from uncontrolled to controlled mid-life and earns a console warning. An input's mode is fixed at mount. Reset an uncontrolled one by changing its key, not by feeding it undefined.

The error boundary that didn't catch

Trap error boundaries catch errors from render, lifecycle, and child construction only. An async failure in a fetch or an event handler slips right past them. Route those through try/catch or a showBoundary call. And one root boundary blanks the whole app on any crash; granularity should match where an error message actually makes sense.

Study React patterns


Red flags across the whole loop: reading state right after setState and expecting the new value; treating a re-render as proof the DOM changed; wrapping everything in useCallback with no memo boundary; suppressing the deps linter; putting server data in a store as "one source of truth." Any one of these caps the score no matter how fluent the rest sounds.

The free notes run from the junior floor to the senior signals, and the diagnostic tells you which band you clear today. Start with React re-renders; it feeds every other topic on this page.

was this useful?

beta

The interviewer part is in the works.

The diagnostic, personal maps, and AI mock interviews are being finished right now. The notes stay free either way. Leave an email and you'll get the first-cohort invite, plus a month of Pro when it opens.