React interview questions come in three kinds, and only one of them is on the question lists. Recall questions ask you to define the virtual DOM. Mechanic questions ask why this
setIntervallogs0forever, and are graded on whether you can name the JavaScript rule underneath. Build questions ask you to writeuseDebouncewith working cleanup before the timer runs out. The mechanic tier is where React rounds are lost, because the failures are JavaScript in a React costume: a closure holding a value from render #1,Object.isdeciding whether a re-render is skipped, a fresh object literal quietly making a dependency array useless. This article sorts the standard question bank into those three types, gives the failing snippet and the fix for each mechanic, covers the build round, and flags the classic answers that went stale between React 17 and 19.2.
The three kinds of React question
Every question in a React loop is graded on one of three things.
Recall. You name or define a concept, and you are graded on vocabulary. "What is reconciliation." "What is a portal." "What are the rules of hooks." There is a correct paragraph, you either have it or you do not, and it takes thirty seconds either way.
Mechanic. You explain or fix a behaviour, and you are graded on whether you can name the JavaScript rule producing it. "This component's interval keeps logging zero, why." "Why does this useMemo recompute on every render when the inputs look identical." The React vocabulary earns partial credit. Naming the closure, the identity comparison, or the microtask ordering earns the rest.
Build. You write working code under a timer, and you are graded by whether it runs. Cleanup on unmount, no stale closure, no race, keyboard behaviour where the task calls for it.
The largest list currently ranking for this query holds 110 questions across seven categories, all in question-and-answer form, with no coding exercise in it. That is a genuinely good recall bank, and the recall tier is real: interviewers do open with definitions, and fumbling "what is a key" is a bad first impression you do not need to have. The problem is that recall is the cheapest tier to prepare and the cheapest to pass, so it does very little to separate candidates. The separation happens in the other two.
The sections below are sorted by those three types, and the sort does not move. "What is the virtual DOM" is recall. "Why does my effect run twice" is mechanic, even though the answer is short. "Write useDebounce" is build.
Recall questions: the bank everyone already has
Compressed answers to the standard set, so you have them. Two or three sentences each is the right length to say out loud, too. The React interview questions guide covers how these get sequenced across a loop.
Virtual DOM and reconciliation. React keeps an in-memory tree of plain objects describing what the UI should look like. On an update it builds a new tree, compares it with the previous one, and applies the differences to the real DOM. Reconciliation is that comparison step, and the payoff is that your code describes the target state instead of the DOM operations to reach it.
Keys. Two rules: a key must be unique among its siblings, and it must not change between renders. Do not generate keys during render, so no key={Math.random()}, because that recreates the components and their DOM on every render and loses user input. Components do not receive key as a prop, so if the child needs the value, pass it again under a different name.
Props vs state, and lifting state up. Props are arguments passed in from the parent and are read-only inside the receiving component. State is owned by the component and persists across its renders. Lifting state up means moving that state to the nearest common ancestor of the components that need it and passing the value plus an updater back down.
Controlled vs uncontrolled inputs. A controlled input takes its value from state, so every keystroke goes through your handler and back. An uncontrolled input keeps its value in the DOM node itself and you read it through a ref or on submit. Passing value with no onChange gets you the read-only warning, because you have asked for control and not supplied any.
Class vs function components. Function components with hooks are the default. The rough mapping for the lifecycle question: componentDidMount corresponds to an effect with an empty dependency array, componentDidUpdate to an effect with dependencies, and componentWillUnmount to the cleanup function the effect returns. Say "rough" out loud, because effects are written to synchronise with a set of values rather than to fire at named moments in a lifecycle.
Rules of hooks. Call hooks at the top level of a component or a custom hook, never inside a condition, loop, or nested function. The reason is call order: React matches each hook call to its stored state by the position of that call in the sequence, so a hook behind an if shifts every later call onto the wrong slot.
Context. createContext plus a provider lets any descendant read a value with useContext without prop drilling. Consumers re-render when the provider's value changes, and the classic trap is building the value object inline in the provider, which produces a new object identity on every render.
Error boundaries. Components that implement getDerivedStateFromError or componentDidCatch and render a fallback when a child throws during rendering. They must be class components. They do not catch errors thrown inside event handlers or in async callbacks.
Portals. Render children into a DOM node outside the parent's position in the document while keeping them in the React tree. That is why a portalled modal still reads context from its React parent and still bubbles events up the React tree rather than the DOM tree.
Code splitting. React.lazy takes a function returning a dynamic import() and <Suspense fallback={...}> renders the fallback while that chunk loads.
Actions. New in React 19: they handle a mutation and its state updates together, with a pending state that starts on submit and resets when the final update commits.
useActionState. Wraps an Action and hands back the last result plus that pending flag, so a form's error message and its spinner come out of the same call.
useFormStatus. Reads the parent <form>'s status as if the form were a context provider, which keeps a submit button from needing props threaded to it.
useOptimistic. Renders a provisional value while the request is in flight and reverts on error.
use. Reads a promise or context during render and, unlike hooks, can be called conditionally, though only in render.
Server Components. Render ahead of time in an environment separate from the client app; only their output reaches the browser, so they cannot use interactive APIs like useState. Anything interactive is composed in as a Client Component with "use client".
Mechanic questions: the JavaScript under the React
Four mechanics cover the questions asked most often here. Each one is a JavaScript rule, and each one has a React symptom that looks like a React bug.
Why does this interval log zero forever?
function Counter() {
const [count, setCount] = useState(0);
useEffect(() => {
const id = setInterval(() => console.log(count), 1000);
return () => clearInterval(id);
}, []);
return <button onClick={() => setCount((c) => c + 1)}>{count}</button>;
}
Click the button and the number on screen climbs. The console keeps printing 0.
Nothing React-specific is happening. count is a const created fresh on each render, and the arrow function passed to setInterval closed over the binding from the render where the effect ran. Empty dependencies mean the effect ran once, so that closure is the only one the interval has:
function render(count) {
return function effect() {
console.log(count);
};
}
const effectFromFirstRender = render(0);
render(1);
render(2);
effectFromFirstRender();
0
Three fixes, and picking the right one is the actual answer. If the interval needs to update the state, use the functional updater, which receives the current value instead of reading a captured one: setCount(c => c + 1). If you only need to read a current value, keep it in a ref that an Effect updates on every render — useEffect(() => { ref.current = count; });, no dependency array — and read ref.current inside the interval callback. The assignment goes in the Effect and not in the render body, because writing a ref during render breaks the purity React expects of a component body. This is the pattern behind useEventCallback, a stable function identity wrapping a body that always sees the latest values. Third, React 19.2 added useEffectEvent for exactly this: an Effect Event always sees the latest props and state, and its identity intentionally changes every render, so it must never appear in a dependency array. It can only be called from inside an Effect (or another Effect Event in the same component) and must not be passed to other components. Those restrictions are enforced by eslint-plugin-react-hooks.
The same closure rule explains a question that sounds different: "why does console.log(count) right after setCount(count + 1) print the old value?" Because count is a binding in the current render's scope, and calling a setter does not reassign it. The new value belongs to the next render.
Why does this useMemo recompute every render?
React decides whether it can skip work by comparing values with Object.is, which differs from === in exactly two cases:
console.log(NaN === NaN, Object.is(NaN, NaN));
console.log(0 === -0, Object.is(0, -0));
console.log({ min: 0 } === { min: 0 });
false true
true false
false
That third line is the one that bites. Every object literal, array literal, and function expression evaluated during render is a new value:
function renderOnce() {
const options = { min: 0, max: 10 };
return options;
}
const first = renderOnce();
const second = renderOnce();
console.log(Object.is(first, second));
console.log(first.min === second.min && first.max === second.max);
false
true
So useMemo(fn, [options]) with options built in the component body recomputes on every render, a React.memo child receiving an inline onSelect={() => ...} re-renders on every parent render, and useCallback exists to give that function a stable identity across renders. Say that identity comparison out loud and the whole memoization family collapses into one idea rather than three APIs to memorise.
The senior version of this question is useSyncExternalStore(subscribe, getSnapshot), which exists so an external store can be read without tearing during concurrent rendering. Tearing is what happens when React pauses part way through a render and the store changes in the gap: two components in the same commit read different values, and the screen shows both at once. Because React compares snapshots with Object.is, a getSnapshot that builds a new object on every call never compares equal, and you get an infinite re-render loop. It has to return cached or immutable data. If you have built something like createGlobalState by hand, you have met this.
What order do these run in?
console.log('render');
setTimeout(() => console.log('timeout 0'), 0);
Promise.resolve().then(() => console.log('promise'));
queueMicrotask(() => console.log('microtask'));
console.log('render end');
render
render end
promise
microtask
timeout 0
Promise callbacks are queued as microtasks and setTimeout schedules a task. Once the current task exits, the event loop drains the entire microtask queue before it picks up the next task, which is why a zero-delay timer still runs last.
The React half is a different mechanism, and saying so is half the answer:
function Row() {
const [count, setCount] = useState(0);
const [flag, setFlag] = useState(false);
function handleClick() {
save().then(() => {
setCount((c) => c + 1);
setFlag((f) => !f);
});
}
// React 17: two renders. React 18 under createRoot: one.
}
React 18 introduced automatic batching. With createRoot, updates inside promises, setTimeout, native event handlers and anywhere else are batched the same way updates inside React event handlers always were. React 17 batched only during React event handlers, which is where the old "two setState calls in a fetch callback cause two renders" answer comes from. The event loop explains why that .then() body runs after the click handler has already returned, which is what put those two updates outside React 17's batched window. It does not explain the batching itself: grouping updates is React's own scheduling decision, not something the microtask queue does. ReactDOM.flushSync() is the opt-out when you need the DOM updated before the next line, for example measuring an element you just revealed.
Why did my input's text follow the wrong row?
Reorder or delete from a list keyed by array index and the rendered text is correct while anything the DOM node owns stays put: an uncontrolled input's typed value, focus, scroll position, a running CSS transition. React matches children to previous children by key among siblings. With index keys the keys are still 0, 1, 2 after the reorder, so React concludes the element at position 0 is the same element as before and only patches its text. The DOM node, and everything the DOM is holding for it, never moves.
The mirror image of this is worth volunteering: because a changed key means a different element, you can use it deliberately to reset state. Rendering <Profile userId={userId} key={userId} /> tells React to treat two profiles with different ids as different components that share no state, which resets the subtree without an effect.
Effects: the question everyone gets half right
Why does my effect run twice? (Mechanic.) In development, StrictMode double-invokes component render functions, state updater and initializer functions (useState, set functions, useMemo, useReducer), ref callbacks, and class constructor, render and shouldComponentUpdate. For every Effect it runs an extra setup, cleanup, setup cycle. All of these checks are development-only and do not affect the production build. The half that candidates miss is the point of it: running setup twice is a test, and an effect that breaks under it is an effect with missing or incorrect cleanup. Answering "it is StrictMode, I'd remove it" is the wrong answer to a question about correctness.
What is the dependency array? A list of values React compares with Object.is against the previous render's list, to decide whether to run cleanup and setup again. It is not a list of triggers, and it is not a list of things you want to happen. That reframing is what makes the object-literal bug obvious and what makes "just remove the dependency to stop the loop" recognisably a bad fix.
Do I need this effect at all? React's own guidance is to calculate derived data at the top level during rendering rather than storing it in state and syncing it with an Effect, because an Effect that immediately sets state restarts the render pass. const fullName = firstName + ' ' + lastName beats a state variable plus an effect. Logic triggered by a user interaction belongs in the event handler, not in an effect watching for the state that the interaction set. Resetting state on a prop change is a key, as above.
Fetching in an effect is the one place effects are still the honest answer in plain React, and the follow-up is always the race. Two requests in flight, the slower one resolves last, and the stale response wins:
useEffect(() => {
let ignore = false;
fetchResults(query, page).then((json) => {
if (!ignore) {
setResults(json);
}
});
return () => {
ignore = true;
};
}, [query, page]);
Every response except the last requested one is ignored. An AbortController in the cleanup is the other accepted answer and additionally cancels the request. Building the caching and deduplication layer on top of this is its own exercise: see Mini React Query Core.
Build questions: what you will actually be asked to write
This is the round the question lists skip entirely. Tasks land in three tiers.
Tier one, JavaScript polyfills. debounce and throttle, Array.prototype.reduce, Promise.all, Function.prototype.call and apply, Array.prototype.find, JSON.stringify. These are graded on edge cases more than on the happy path: sparse arrays and the missing-initial-value throw for reduce, this forwarding and cancellation for debounce, empty input and rejection ordering for Promise.all.
Array.prototype.myReduce = function (callback, ...initial) {
const arr = Object(this);
const len = arr.length >>> 0;
let acc;
let i = 0;
if (initial.length > 0) {
acc = initial[0];
} else {
while (i < len && !(i in arr)) i++;
if (i >= len) {
throw new TypeError('Reduce of empty array with no initial value');
}
acc = arr[i++];
}
for (; i < len; i++) {
if (i in arr) {
acc = callback(acc, arr[i], i, arr);
}
}
return acc;
};
The rest parameter is there so an explicitly passed undefined still counts as an initial value, and the two i in arr guards are what make [, , 1].myReduce(fn) return 1 without ever calling fn. Miss either and the happy path still passes. The JavaScript coding interview guide covers this tier in depth.
Tier two, custom hooks. useDebounce, usePrevious, useLocalStorage, useEventListener, useMap, useTextareaAutosize, a fetch hook with cancellation, and useWhyDidYouUpdate as the debugging variant. The shape is nearly always the same, and the marks are in the last three lines:
function useDebounce(value, delay) {
const [debounced, setDebounced] = useState(value);
useEffect(() => {
const id = setTimeout(() => setDebounced(value), delay);
return () => clearTimeout(id);
}, [value, delay]);
return debounced;
}
Tier three, components. An accordion or FAQ disclosure, tabs, a star rating, a todo list, an autocomplete with async suggestions.
function Accordion({ items }) {
const [openId, setOpenId] = useState(null);
const headers = useRef([]);
function onKeyDown(event, index) {
const step =
event.key === 'ArrowDown' ? 1 : event.key === 'ArrowUp' ? -1 : 0;
if (step === 0) return;
event.preventDefault();
headers.current[(index + step + items.length) % items.length]?.focus();
}
return items.map((item, index) => (
<div key={item.id}>
<h3>
<button
ref={(node) => {
headers.current[index] = node;
}}
id={`${item.id}-header`}
aria-expanded={openId === item.id}
aria-controls={`${item.id}-panel`}
onClick={() => setOpenId(openId === item.id ? null : item.id)}
onKeyDown={(event) => onKeyDown(event, index)}
>
{item.title}
</button>
</h3>
<div
id={`${item.id}-panel`}
role="region"
aria-labelledby={`${item.id}-header`}
hidden={openId !== item.id}
>
{item.body}
</div>
</div>
));
}
The marks are in the aria-expanded that tracks open state, the aria-controls and aria-labelledby pair tying trigger to panel, and the trigger being a real <button> inside a heading, which is what gets you Enter and Space for free. Up and Down moving focus between headers is not part of the APG accordion pattern — Tab plus Enter or Space is — but it costs four lines and shows you thought past the click handler.
What is being checked, whether by a human or a test suite: it runs. Timers, intervals, subscriptions and listeners are cleaned up on unmount. A stale async response cannot overwrite a fresh one. Controlled inputs are wired in both directions. Nothing derivable from props is duplicated into state. For interactive components, keyboard behaviour and the relevant aria attributes, because arrow keys on tabs and aria-expanded on a disclosure are the first thing a UI-focused interviewer looks for.
A workable split for a 25-minute task: two or three minutes restating the requirements and asking about the edge cases you intend to skip, fifteen writing the straightforward version, and the remainder testing it against your own list of failure cases out loud. Finishing a clean version with five minutes of visible testing reads far better than a half-finished ambitious one.
Answers that have gone stale (React 17 to 19.2)
Several answers still circulating were correct when written and are now wrong. Giving one is a strong signal about when you last read the docs.
Event pooling. React 17 removed it. The old advice to call e.persist() before reading a synthetic event asynchronously no longer applies. React 17 also changed event delegation: instead of attaching handlers at the document level, React attaches them to the root DOM container the tree is rendered into, which is what makes multiple React versions on one page workable.
"setState is always asynchronous, and batching only happens in React event handlers." React 18's automatic batching means that under createRoot, updates inside promises, setTimeout, native event handlers and anywhere else are batched too. flushSync is the documented opt-out.
forwardRef. In React 19, function components receive ref as a regular prop, so new components no longer need it. The release post says forwardRef will be deprecated and removed in a future version, and promised a codemod; it now exists separately, as npx codemod react/19/remove-forward-ref in reactjs/react-codemod. Existing forwardRef code still works, so "it was removed" is an overstatement worth avoiding. React 19 also allows a ref callback to return a cleanup function.
useFormState. Renamed to useActionState in React 19.
"Wrap everything in useMemo and useCallback." React Compiler v1.0 was released on 7 October 2025. It is a build-time tool that memoizes automatically, including cases manual memoization cannot cover, such as a value computed after an early return. It installs as babel-plugin-react-compiler and is opt-in, and its diagnostics ship through eslint-plugin-react-hooks' recommended presets. useMemo and useCallback remain available as escape hatches for precise control. In the Meta Quest Store, initial loads and cross-page navigations improved by up to 12% and some interactions became more than 2.5 times faster, with neutral memory usage. The interview-relevant version of this answer: you still need to explain identity comparison, because that is what the compiler is automating.
useEffectEvent. Added in React 19.2, alongside <Activity />, cacheSignal, Performance Tracks in Chrome DevTools, and Partial Pre-rendering. If your stale-closure answer stops at "use a ref", adding this shows currency.
As of August 2026 the current stable line is 19.2.x. React 19.2.0 shipped on 1 October 2025, with patch releases through 19.2.8 on 21 July 2026.
Saying it out loud
A recall or mechanic answer that lands has four beats, in this order: definition, mechanism, cost or trade-off, when it bites in real code. Sixty to ninety seconds total. The last beat is what makes an answer sound like experience rather than revision, and most candidates skip it.
On a recall question, "what is React.memo": it skips re-rendering a component when its props compare equal to the previous props (definition). The comparison is shallow and uses Object.is per prop (mechanism). It costs a comparison on every render and does nothing if any prop is a fresh object or function each time, which is the usual reason it appears to do nothing (trade-off). It earns its place on a component that renders a large list and sits under a parent that re-renders often (when it bites).
On a mechanic question, "why does this interval log zero": the callback closed over count from the render where the effect ran, and empty dependencies mean that effect never re-ran (definition and mechanism together). Adding count to the dependencies fixes the value but tears down and recreates the interval on every tick's worth of state change (trade-off). So use the functional updater if the interval is updating that state, or a ref, or useEffectEvent, depending on whether you need to write or only read (the fix). It bites on anything set up once and left running: a polling widget still reporting the filter it mounted with, an autosave timer, a socket handler registered in an empty-deps Effect (when it bites).
When you genuinely do not know: say so in one sentence, then say what you would expect and how you would check. "I have not used useSyncExternalStore in production. I know it exists so an external store can be read without tearing, and I would expect the snapshot function to need a stable return value. I would check the reference page before wiring one up." That reads as calibration. Guessing confidently and being wrong reads as something else.
In a build round, narrate at the decision points and stay quiet in between. Say what you are about to write and why, then write it. Silence for five straight minutes leaves the interviewer with nothing to grade except the final code.
How to practise this
Drill each tier in the way it is graded, because they are not the same skill.
Recall: work from a list, answer out loud, in four beats, timed. Reading the answer and nodding is not the same exercise.
Mechanic: predict the output before you run it. Take a snippet, write down what you think it prints and which rule decides it, then run it. The gap between your prediction and the real output is precisely the material you are missing, and nothing else surfaces it as cheaply.
Build: a timer, and tests you did not write. Grading your own solution is how cleanup and race handling get quietly skipped, since your own mental test suite tends to only run the happy path. That is what UIReady Premium, lifetime access is for: interview questions in a full Sandpack workspace, graded by real Jest tests including timer mocks, with most questions available in React, Vue, Angular and vanilla TypeScript so you can practise in whichever one you will be asked about.