React Interview Questions for Senior Developers — Hooks, Reconciliation, Fiber Architecture, Performance (Deep Answers)
30 senior React interview questions with deep answers across Hooks, Reconciliation, Fiber Architecture, and Performance — the mental model interviewers test.
- Author
- Randhir Jassal
- Published
- Reading time
- 38 min read
- Views
- 6 views
React Interview Questions for Senior Developers — Hooks, Reconciliation, Fiber Architecture, Performance (Deep Answers)
Senior React interviews don''t ask "what is useState." They ask why hooks have to be called in the same order, what a Fiber node actually contains, why
React.memois sometimes a regression, and what changed in concurrent rendering. The questions are the same shape the React team thinks in — and the people who can answer them well are the people who can debug production reconciliation bugs and reason about render performance instead of guessing.This guide collects the 30 deep questions you''ll actually see in senior React interviews in 2026, organized into four blocks: Hooks, Reconciliation, Fiber Architecture, and Performance. Each answer is the level of depth a senior should give — not a textbook definition, but the model that lets you reason about edge cases at the whiteboard.
How to use this guide
For each question, the answer has three parts:
- Short answer — what you''d say first.
- Deep dive — the model behind it, with code where it helps.
- Senior signal — what an interviewer is really listening for. (Often the why, not the what.)
Skim the questions; read the answers cold; then practice giving the short answer in two sentences and following up with the deep dive only when prompted. That''s the rhythm of senior interviews.
Block 1 — Hooks
Q1. Why must hooks be called in the same order every render?
Short answer. React doesn''t store hook state by name — it stores it by call order in a linked list attached to the fiber. The order is the only way React knows which call gets which state slot.
Deep dive. Internally, each fiber has a memoizedState field that''s the head of a linked list of hook records:
fiber.memoizedState → [hook0] → [hook1] → [hook2] → null
useState useEffect useMemo
On each render, React walks this list as your component calls useState, useEffect, etc. — assigning each call to the next node in the list. If you call hooks conditionally:
function Buggy({ flag }) {
const [a, setA] = useState(0);
if (flag) useEffect(() => { /* ... */ }, []); // conditional
const [b, setB] = useState(0);
// When flag flips, b gets the slot that previously belonged to the effect.
}
That''s why the Rules of Hooks aren''t a style suggestion — they''re load-bearing.
Senior signal. Mentioning the linked-list / call-index data structure (not just "it breaks"). Bonus: explaining why React can''t use names — because hooks like useState are called the same way many times and have no identity tag.
Q2. What does the lazy form of useState(() => …) do and why does it matter?
Short answer. It runs the initializer only on the first render, instead of every render. Matters when computing the initial state is expensive or has side effects.
Deep dive.
// BAD — runs expensiveCompute() on every render
const [data, setData] = useState(expensiveCompute(props));
// GOOD — runs once, on the initial render only
const [data, setData] = useState(() => expensiveCompute(props));
The second form passes a function React calls once. The same pattern exists for useReducer(reducer, initialArg, init) where init(initialArg) computes the initial state lazily.
Senior signal. Recognizing this also matters for localStorage.getItem and any work that touches DOM / sync APIs on initial render.
Q3. useEffect vs useLayoutEffect — when does picking wrong cause a visual bug?
Short answer. useLayoutEffect runs synchronously after DOM mutations but before the browser paints. useEffect runs after paint. If you measure or mutate the DOM in useEffect and use that to set state, the user sees a flash of the wrong frame.
Deep dive.
function Tooltip({ children }) {
const ref = useRef<HTMLDivElement>(null);
const [pos, setPos] = useState({ left: 0, top: 0 });
useEffect(() => { // flashes at (0, 0) for one frame
const rect = ref.current!.getBoundingClientRect();
setPos({ left: rect.left, top: rect.top - 8 });
}, []);
// useLayoutEffect would set pos BEFORE the browser paints → no flash.
}
Use useLayoutEffect for DOM measurement / mutation that affects layout. Use useEffect for everything else (data fetching, subscriptions, logging). useLayoutEffect blocks paint — overuse hurts perceived performance.
Senior signal. Knowing the SSR caveat: useLayoutEffect doesn''t run on the server and React warns about it. The fix is useIsomorphicLayoutEffect (a tiny isomorphic wrapper).
Q4. Why does useMemo not guarantee memoization?
Short answer. React explicitly reserves the right to drop memoized values to free memory. useMemo is a hint, not a contract.
Deep dive. From the React docs:
"You may rely on
useMemoas a performance optimization, not as a semantic guarantee. In the future, React may choose to ''forget'' some previously memoized values and recalculate them on next render."
So useMemo and useCallback are correctness-neutral — your component must work even if React re-runs the function every time. Never use useMemo to ensure referential equality for something correctness depends on. For that, store the value in a useRef or move it outside the component.
Senior signal. Mentioning that the React Compiler (React 19+) auto-memoizes correctly — so useMemo/useCallback are increasingly a "let the compiler handle it" concern.
Q5. What is the stale closure problem and how do you fix it?
Short answer. A function captured in useEffect/useCallback closes over the props/state from the render in which it was created. If those change, the captured function still sees the old values.
Deep dive.
function Counter() {
const [count, setCount] = useState(0);
useEffect(() => {
const id = setInterval(() => {
console.log(count); // always logs 0 — closure captured the first render
}, 1000);
return () => clearInterval(id);
}, []); // empty deps → effect runs once with count=0 forever
}
Three fixes:
// 1. Add count to deps (effect re-runs; cleanup → re-subscribe)
useEffect(() => {
const id = setInterval(() => console.log(count), 1000);
return () => clearInterval(id);
}, [count]);
// 2. Use the updater form when the effect doesn''t need to read state
useEffect(() => {
const id = setInterval(() => setCount((c) => c + 1), 1000);
return () => clearInterval(id);
}, []);
// 3. Use a ref to always read the latest value (escape hatch)
const countRef = useRef(count);
useEffect(() => { countRef.current = count; });
useEffect(() => {
const id = setInterval(() => console.log(countRef.current), 1000);
return () => clearInterval(id);
}, []);
Senior signal. Knowing that "just add it to deps" is the correct default, and option 3 is an escape hatch that hides a design problem most of the time.
Q6. When is useCallback actually pointless?
Short answer. When the callback isn''t passed to a memoized child (no React.memo or no useEffect dep) — there''s no consumer that would benefit from referential stability.
Deep dive.
// useCallback does NOTHING useful here — there''s no memoized consumer
function Useless({ items }) {
const handleClick = useCallback((id) => alert(id), []);
return <ul>{items.map(i => <li key={i.id} onClick={() => handleClick(i.id)}>{i.name}</li>)}</ul>;
// We wrap in useCallback then *immediately* wrap in another inline arrow.
}
useCallback only pays off if the exact identity it returns flows into something that cares (memoized child component, effect dep, hook input). Otherwise it''s overhead for zero benefit.
Senior signal. Reaching for the React Compiler / "auto-memoization" as the modern answer, instead of religiously wrapping every callback by hand.
Q7. What does useReducer give you that useState doesn''t?
Short answer. State transitions are described by an external pure function that you can unit-test, share, and reason about — independent of the component. Plus updaters can dispatch from anywhere with dispatch instead of needing access to a setter per piece of state.
Deep dive.
type State = { items: Item[]; selectedId: string | null };
type Action =
| { type: 'add'; item: Item }
| { type: 'remove'; id: string }
| { type: 'select'; id: string };
function reducer(state: State, action: Action): State {
switch (action.type) {
case 'add': return { ...state, items: [...state.items, action.item] };
case 'remove': return { ...state, items: state.items.filter(i => i.id !== action.id) };
case 'select': return { ...state, selectedId: action.id };
}
}
function List() {
const [state, dispatch] = useReducer(reducer, { items: [], selectedId: null });
}
Senior signal. Noting that useReducer + Context is the "poor man''s Redux" pattern and discussing when it''s enough vs when you need Zustand/Redux Toolkit.
Q8. useTransition vs useDeferredValue — what''s the actual difference?
Short answer. Both mark work as low-priority, but useTransition lets you mark a state update as non-urgent at the call site, while useDeferredValue lets a child defer reading a value it doesn''t control.
Deep dive.
// useTransition — you own the setter
function Search({ allItems }) {
const [query, setQuery] = useState('');
const [filtered, setFiltered] = useState(allItems);
const [isPending, startTransition] = useTransition();
function onChange(e) {
setQuery(e.target.value); // urgent — keep input snappy
startTransition(() => setFiltered(allItems.filter(i => i.name.includes(e.target.value))));
}
}
// useDeferredValue — you don''t own the setter (value comes from props)
function SearchResults({ query }) {
const deferredQuery = useDeferredValue(query);
const results = useMemo(() => expensiveFilter(deferredQuery), [deferredQuery]);
}
Both end up scheduling the work at a lower priority lane. Pick useTransition when you control the setter; useDeferredValue when you don''t.
Senior signal. Mentioning that both rely on interruptible rendering — Concurrent React can throw away in-progress low-priority work if a higher-priority update arrives.
Q9. What problem does useSyncExternalStore solve?
Short answer. It safely subscribes to an external store from a React component during concurrent rendering, eliminating tearing (different components seeing different snapshots of the same store within one render pass).
Deep dive. Before React 18, libraries like Redux subscribed in useEffect and called forceUpdate. With concurrent rendering, two components can read the store at different points in time within the same render; the result can be a UI that shows mismatched data for a frame.
import { useSyncExternalStore } from 'react';
function useCart() {
return useSyncExternalStore(
cartStore.subscribe, // register a listener; returns unsubscribe
cartStore.getSnapshot, // current snapshot (sync)
cartStore.getServerSnapshot, // SSR snapshot (optional)
);
}
React guarantees all consumers in a render pass see the same snapshot — no tearing.
Senior signal. Knowing this is mostly relevant when building a state library — application code rarely calls it directly. Every modern state lib (Zustand, Jotai, Redux Toolkit) uses it under the hood.
Q10. What''s the difference between useEffect cleanup and useEffect re-running?
Short answer. Cleanup runs before the next effect, or when the component unmounts. Critical when the effect subscribes to something — without cleanup you leak handlers.
Deep dive.
useEffect(() => {
const id = setInterval(() => console.log(value), 1000);
return () => clearInterval(id);
}, [value]);
// When value changes:
// 1. React calls the cleanup of the PREVIOUS effect (clears old interval).
// 2. React runs the NEW effect (sets up new interval with new value).
// If you forgot the cleanup, you''d have an interval per render — memory + work leak.
Senior signal. Mentioning that StrictMode in dev runs the effect twice (mount → cleanup → mount) to surface missing cleanups. This is why "my effect runs twice in dev" is a feature, not a bug.
Block 2 — Reconciliation
Q11. What is reconciliation and how does React decide whether to update vs re-mount?
Short answer. Reconciliation is the algorithm that diffs the new element tree against the previous one and translates the diff into DOM operations. Two elements at the same position are considered "the same" only if their type is identical; otherwise React unmounts the old and mounts the new.
Deep dive.
// Same type → update in place (props change, state preserved)
<Counter value={1} /> → <Counter value={2} />
// Different type → unmount Counter, mount Display (state is lost)
<Counter value={1} /> → <Display value={1} />
// Same type but different KEY → unmount + mount, even though types match
<li key="a" /> → <li key="b" />
For lists, keys are how you tell React "this is the same logical item across renders" so it updates in place instead of re-mounting.
Senior signal. Calling out that position matters too: {flag ? <A /> : <B />} re-mounts whichever is present even if you''d intuit they "swap." React reconciles by position + type, not by what you meant.
Q12. Why does changing a component''s key force a re-mount, and when is that useful?
Short answer. Because key is part of how React identifies "same component." Changing the key tells React "this is a new logical component" — old state is thrown away, new component is mounted fresh. Useful for resetting state without imperative reset() methods.
Deep dive.
// Form preserves its state across user changes — that''s USUALLY what you want
<Form />
// Force the form to reset when the user changes:
<Form key={userId} />
// When userId changes, React unmounts old <Form/> and mounts a new one.
This is the idiomatic way to reset a subtree in React. No imperative ref method needed.
Senior signal. Knowing the same trick is used for <Suspense> boundaries that restart a fetch when their key changes.
Q13. What is the "two-tree" model in React?
Short answer. React maintains two fiber trees: the current tree (what''s painted) and the work-in-progress tree (what''s being built). When work finishes, React commits the work-in-progress tree as the new current. This is double buffering at the fiber level.
Deep dive.
current tree work-in-progress tree
Fiber A Fiber A''
│ │
Fiber B Fiber B''
│ │
Fiber C Fiber C''
┌──────────────────────────────────┐
│ React builds the WIP tree │
│ (interruptible, can yield) │
└────────────┬─────────────────────┘
│ all work done
▼
┌──────────────────────────┐
│ COMMIT phase (sync, atomic)│
│ swap pointers; WIP → current│
└──────────────────────────┘
This is what makes interruptible rendering safe: if React abandons a work-in-progress tree, the current tree (what the user sees) is untouched.
Senior signal. Connecting this to why React''s commit phase must be synchronous — once you start touching the DOM, you can''t be interrupted halfway without showing an inconsistent UI.
Q14. Why is using an array index as a list key sometimes a bug?
Short answer. When the list can reorder or have items inserted/deleted, index keys make React reuse the wrong DOM/state. Stable, item-identifying keys (e.g., id) make React reconcile correctly.
Deep dive.
// 3 inputs with index keys, then user inserts a new item at the top
[A, B, C] → [NEW, A, B, C]
// With keys 0, 1, 2:
// key 0: was <A/>, now <NEW/> → React mutates A in place, state belongs to A
// key 1: was <B/>, now <A/> → A''s input state is now showing B''s text
// key 2: was <C/>, now <B/> → B''s input state is showing C''s text
// key 3: NEW → C unmounts (state lost), fresh input created
// Result: input contents shift visually but DOM state doesn''t move with them.
With key={item.id}, React correctly maps each input to its item.
Senior signal. Acknowledging that index keys are fine when the list never reorders and only appends (e.g., chat messages by time).
Q15. What''s the difference between "elements" and "fibers"?
Short answer. Elements are the lightweight, immutable description of what you want ({type: Counter, props: {...}}) — what your JSX returns. Fibers are the long-lived, mutable internal nodes React uses to track state, effects, and reconciliation work for each element.
Deep dive.
// JSX
<Counter value={1} />
// Compiles to an element (a plain object)
{ type: Counter, props: { value: 1 }, key: null, ref: null, $$typeof: Symbol(react.element) }
// React creates a fiber for it on first render
{
type: Counter,
stateNode: instance | dom,
memoizedState: {...}, // hooks list head
memoizedProps: { value: 1 },
child / sibling / return,
flags: 0,
alternate: workInProgress,
lanes: 0,
}
Elements are produced on every render and thrown away. Fibers persist across renders and accumulate state.
Senior signal. Mentioning that elements are cheap (just objects) — which is why "creating an element on every render" isn''t a perf problem; the expensive part is the reconciler''s work.
Block 3 — Fiber Architecture
Q16. What''s a Fiber and why was it introduced?
Short answer. A Fiber is a unit of work that represents one node in the React tree. The Fiber architecture (React 16+) replaced the old recursive "stack reconciler" so reconciliation could be interruptible, prioritized, and resumable — the foundation for Concurrent React.
Deep dive. The old stack reconciler did all the diffing in one synchronous recursive pass — once started, it ran until done, blocking the browser. With Fiber, reconciliation is a loop that processes one fiber at a time, can yield to the browser between fibers, and remembers where to resume.
Stack reconciler (old): Fiber reconciler (new):
reconcile(rootElement) while (workInProgress && !shouldYield()) {
recursively process tree workInProgress = performUnitOfWork(workInProgress);
return when done (blocking) }
// yields back to the browser if needed
requestIdleCallback(workLoop);
This is what unlocked everything from Suspense to useTransition to streaming SSR.
Senior signal. Naming the two phases: render (interruptible, can be discarded) and commit (synchronous, side effects applied to DOM). And knowing that hooks were possible because fibers gave each component a stable place to store state across renders.
Q17. What does a Fiber node actually contain?
Short answer. A type, a pointer to its DOM node / class instance, props, hooks list, sibling/child/return pointers (tree links), flags (what work to do), lanes (priority info), and an alternate pointer to its twin in the other tree.
Deep dive. Annotated:
{
// Identity
type, key, elementType,
// Tree links
return, // Parent fiber
child, // First child
sibling, // Next sibling
// Data
pendingProps,
memoizedProps,
memoizedState, // Hooks list (head node) or class state
stateNode, // DOM node, class instance, or null
// Effects / scheduling
flags, // Bitfield: Placement | Update | Deletion | etc.
lanes, // Priority info
childLanes,
updateQueue,
// Double buffering
alternate, // Pointer to this fiber in the other tree
}
alternate is the bridge between the current tree and the work-in-progress tree.
Senior signal. Knowing that memoizedState is the hook list head — connecting the Fiber architecture back to why hook order matters (Q1).
Q18. What is the work loop and how does it yield?
Short answer. React processes fibers one at a time in a loop. After each unit of work, it checks shouldYield() (based on a 5ms time budget); if yes, it pauses and lets the browser handle input/paint, then resumes via MessageChannel.
Deep dive. Pseudocode:
function workLoop(deadline) {
while (workInProgress !== null && !shouldYield(deadline)) {
workInProgress = performUnitOfWork(workInProgress);
}
if (workInProgress !== null) {
scheduleCallback(workLoop); // come back when browser is idle
} else {
commitRoot(); // all work done; commit to DOM
}
}
Yielding is what enables Concurrent React to keep an input responsive while doing heavy rendering — the typing handler runs in the gap between two fibers.
Senior signal. Mentioning that shouldYield uses scheduler with MessageChannel for low-overhead task scheduling — not requestIdleCallback, which has too coarse a granularity.
Q19. What are lanes and what problem do they solve?
Short answer. Lanes are a bitmask scheduling model. Each render has a set of "lanes" (priorities) — sync, default, transition, idle, etc. Updates land on lanes; React processes higher-priority lanes first and can interleave them. Lanes replaced the earlier "expiration time" model in React 17+.
Deep dive. Conceptually:
SyncLane (urgent — click, keystroke)
InputContinuousLane (drag, hover)
DefaultLane (typical updates)
TransitionLane (startTransition, useTransition)
RetryLane (Suspense retries)
IdleLane (lowest)
A fiber''s lanes bitfield tells React which priority levels have pending work for it. This is how startTransition works without freezing the input.
Senior signal. Connecting lanes to useDeferredValue — the deferred update lands on a transition lane and is interruptible.
Q20. How does Concurrent React differ from Legacy React?
Short answer. Legacy React renders synchronously: once started, it blocks until done. Concurrent React (default in React 18+) can render in pieces, pause, resume, throw work away, and prioritize urgent work over non-urgent.
Deep dive. What you get with concurrent rendering:
- Interruptible rendering —
useTransition/useDeferredValue. - Suspense for data fetching (server + client).
- Streaming SSR — flush HTML as data resolves.
- Automatic batching — multiple
setStatecalls in the same event are coalesced everywhere.
In Concurrent React, your component function can be called multiple times for the same render before commit (because React can throw work away and restart). So component functions must be pure — no side effects in render.
Senior signal. Mentioning that opt-in for Concurrent React is creating a root with createRoot — once you do that, all the concurrent features are active.
Q21. What happens in the render phase vs the commit phase?
Short answer. Render is where React calls your functions, runs hooks, and builds the work-in-progress tree. It''s interruptible and pure (no DOM changes). Commit is where React applies the changes to the DOM, runs refs, runs useLayoutEffect, and (after paint) runs useEffect. It''s synchronous and atomic.
Deep dive.
RENDER PHASE COMMIT PHASE
───────────── ─────────────
- Call components - Apply DOM mutations
- Run useState / useReducer / etc. - Update refs
- Build WIP fiber tree - Run useLayoutEffect synchronously
- Compute diffs - (paint)
- Interruptible - Run useEffect after paint
- Pure (no side effects allowed) - Synchronous, atomic
- May run multiple times - Runs exactly once per committed render
Putting side effects in render is the classic bug — they may run multiple times in concurrent mode. Put them in useEffect / useLayoutEffect.
Senior signal. Knowing that event handlers run between render and commit phases of subsequent renders.
Q22. What is "tearing" and how does Concurrent React handle it?
Short answer. Tearing is when two components in the same render show data from two different snapshots of an external store — a visible inconsistency. Concurrent React''s interruptible rendering created the possibility of tearing; useSyncExternalStore is the API that prevents it for store libraries.
Deep dive. Without useSyncExternalStore:
At t=0, store value is X. React starts rendering tree.
At t=10ms, halfway through the render, the store updates to Y.
Components rendered before t=10ms saw X; components rendered after saw Y.
The committed UI mixes X and Y — tearing.
useSyncExternalStore guarantees all consumers in a single render see the same snapshot.
Senior signal. Mentioning that this only mattered with Concurrent React — in legacy synchronous render, tearing was impossible.
Block 4 — Performance
Q23. How do you find a slow React component?
Short answer. Use the React DevTools Profiler — record an interaction, look at the flamegraph for the component whose render dominates, and use the "what caused this render" feature to find the trigger. Pair with the browser''s Performance tab to find non-React causes.
Deep dive. A senior workflow:
- Reproduce the slow interaction with a recording.
- Sort by self render time — find the actual culprit, not the parent.
- Check "why did this render?" in the DevTools side panel.
- Decide the fix:
- Pure render is slow → memoize the computation or split the component.
- Re-renders unnecessarily →
React.memo(with stable props) or move state down. - Big list → virtualization.
- Big tree fan-out → context with selectors or move to Zustand.
// Programmatic measurement in production
import { Profiler } from 'react';
function onRender(id, phase, actualDuration) {
if (actualDuration > 16) analytics.track('slow_render', { id, phase, ms: actualDuration });
}
<Profiler id="DataTable" onRender={onRender}><DataTable /></Profiler>
Senior signal. Saying "I''d profile before optimizing" — most "performance fixes" are guesses that either do nothing or make things worse.
Q24. What''s the actual cost of useMemo / useCallback / React.memo?
Short answer. Each adds a comparison + memory cell. Cheap individually, not free in aggregate. If the wrapped computation/render is itself cheap, the wrapper costs more than it saves.
Deep dive. Each memo has three costs:
- The comparison — props/deps shallow-compared on every render.
- The closure / cell — held in the fiber''s memoizedState.
- The cognitive overhead — readers must understand why it''s there.
When does memo not hit?
- Inline objects/arrays as props (
prop={{ a: 1 }}creates a new object each render). - Children prop (different element tree each render = different reference).
- Dep array contains a value that changes every render.
// useless memo — children prop is always a new element tree
const Memoed = React.memo(function (props) { return <div>{props.children}</div> });
<Memoed><Counter /></Memoed> // re-renders every parent render anyway
Senior signal. Mentioning that the React Compiler (React 19+) is designed to add memoization where it actually pays off, by static analysis.
Q25. Why is React.memo sometimes a performance regression?
Short answer. Because the props comparison itself isn''t free, and if the props are not referentially stable, the memo never hits — you pay the cost of the comparison plus the cost of re-rendering anyway.
Deep dive.
// REGRESSION — memo cost added, never helps
const Memo = React.memo(Display);
function Parent() {
return <Memo data={{ a: 1 }} />; // new object every render → memo always misses
}
// HELP — props are stable references
const Memo = React.memo(Display);
function Parent() {
const data = useMemo(() => ({ a: 1 }), []);
return <Memo data={data} />; // same reference → memo hits
}
The rule: only wrap with React.memo if (a) the wrapped component is expensive to render and (b) its props are referentially stable.
Senior signal. Connecting this to measurement — "I''d verify with the Profiler that the memo actually prevents renders."
Q26. What''s the perf model of Context, and when does it cause unnecessary renders?
Short answer. When a context''s value identity changes, every component reading that context re-renders, regardless of whether the part they care about changed. Contexts don''t support selectors.
Deep dive.
// BAD — every consumer re-renders when ANY part of state changes
const AppCtx = createContext({ user, theme, notifications });
// BETTER — split contexts by change frequency
const UserCtx = createContext(user); // changes rarely
const ThemeCtx = createContext({ theme, set }); // changes occasionally
const NotifyCtx = createContext(notifications); // changes often
// BEST for high-frequency selector-based state → use Zustand/Redux/Jotai instead
Senior signal. Stating that Context is a great provider mechanism but a poor state-management substitute for high-frequency state.
Q27. What is virtualization and when is it the answer?
Short answer. Virtualization renders only the rows the user can see (~30) while the browser thinks it''s scrolling all N (50,000+). It''s the single biggest client-side win for any list / grid above ~200 rows.
Deep dive.
import { useVirtualizer } from '@tanstack/react-virtual';
const v = useVirtualizer({ count: rows.length, estimateSize: () => 44, overscan: 8, getScrollElement: () => parent });
v.getVirtualItems().map(item => <Row row={rows[item.index]} style={{ transform: `translateY(${item.start}px)` }} />)
When is it the answer? When DOM node count or React reconciliation work is the bottleneck. When it''s not: when the rows themselves are heavy components; or when search needs to find rows not in the DOM.
Senior signal. Pairing virtualization with server-side pagination so you don''t load all data into memory just to virtualize it.
Q28. Why doesn''t useState''s updater see the latest state immediately?
Short answer. useState''s setter schedules an update; the new value is only available on the next render. Inside the same handler, count is the captured snapshot. Use the updater form (setCount((c) => c + 1)) when you need to reference the previous value.
Deep dive.
function Buggy() {
const [count, setCount] = useState(0);
function increment3() {
setCount(count + 1); // schedules: "set to 0+1 = 1"
setCount(count + 1); // schedules: "set to 0+1 = 1" (still sees 0!)
setCount(count + 1); // schedules: "set to 0+1 = 1"
// After commit, count is 1, not 3.
}
function increment3Correctly() {
setCount((c) => c + 1); // schedules: c => c+1
setCount((c) => c + 1);
setCount((c) => c + 1);
// After commit, count is 3.
}
}
Senior signal. Mentioning that this is also why "automatic batching" (React 18) coalesces all the setters into one render. The updater form remains the safe pattern.
Q29. How does the React Compiler change performance work?
Short answer. It auto-memoizes components and values at build time, eliminating the need for most useMemo/useCallback/React.memo hand-wrapping. The output is what you''d write if you were perfectly disciplined about memoization — without the noise.
Deep dive. The win is twofold: less manual memo noise in your code, and the compiler can detect dependencies precisely (no human-maintained deps array to get wrong). For React 19+ apps, "wrap everything in useMemo just in case" is obsolete.
Senior signal. Knowing what the compiler won''t do: it doesn''t fix bad data shapes (a 12,000-row table still needs virtualization), and it doesn''t replace structural perf wins like code splitting and server components.
Q30. What''s the difference between client rendering, SSR, and Server Components — for perf?
Short answer.
- CSR — the browser downloads JS, executes it, fetches data, renders. Slow first paint, fast nav.
- SSR — server renders HTML; browser hydrates. Faster first paint; hydration is still a cost.
- Server Components (RSC) — server renders the parts that don''t need interactivity; no JS is shipped for them. Smaller bundle; data fetch happens server-side; only interactive parts hydrate.
Deep dive. For an app with a static dashboard shell + a few interactive widgets:
CSR: ship JS for everything → browser fetches data → browser renders.
SSR: server renders all → ship HTML + ship JS for all → hydrate all.
RSC: server renders static parts (no JS ships for them) + client components ship JS for interactivity.
RSC wins on bundle size (often −60–80% for shell-heavy apps) and time to first byte of content.
Senior signal. Mentioning that the modern Server Components + Client Components split is the contemporary form of the old "Container/Presentation" pattern.
Bonus — the meta question
Q31. "What''s the most impactful React perf optimization you''ve shipped?"
This is the question seniors are most likely to be asked. A strong answer has four parts:
- What was slow (with a metric: TTI 4.2s, INP 380ms, table mount 1.4s).
- How you measured (Profiler, Lighthouse, RUM).
- What you changed (virtualization, code splitting, RSC, memoization).
- What it produced (the metric, after).
Strong example:
"Our analytics dashboard was hitting TTI 4.1s and INP 380ms on a 12,000-row table. I profiled and found the table re-rendered in full on every filter keystroke. I added
@tanstack/react-virtual(rows in DOM 12k → 30), wrapped the row component inReact.memowith stable callbacks viauseCallback, and moved the filter touseDeferredValue. TTI dropped to 1.3s, INP to 90ms. The user-reported slow-grid tickets went to zero."
The pattern: measure, identify the structural cause, apply the matching technique, re-measure.
Closing — what interviewers are actually testing
For senior React roles, the bar isn''t memorizing trivia — it''s the mental model:
- Can you reason about why hooks have to be called in order?
- Can you predict whether a refactor will cause unnecessary re-renders?
- Can you decide between server pagination and virtualization for a 50k-row table?
- Can you read a Profiler flamegraph and identify the structural issue (not just guess
useMemo)?
If you can do those, you''ll do well even on questions you''ve never seen — because the model lets you derive the answer. That''s the senior signal interviewers are listening for.
Further reading
- React docs — Render and Commit — the canonical mental model.
- Lin Clark — A Cartoon Intro to Fiber — the classic talk on the Fiber reconciler.
- Dan Abramov — Beyond React 16 — the "why concurrent rendering" talk.
- React docs — useSyncExternalStore — the tearing-free subscription model.
- React Compiler docs — the React 19 auto-memoization story.
Prepping for a senior React interview and want a mock with these questions, or stuck explaining a topic in the depth they''re asking for? Email randhir.jassal@gmail.com with the role and which question you''d like to walk through — happy to do a deep-dive on any of them.
Get the next issue
A short, curated email with the newest posts and questions.