30% offEnding soon

React Interview Questions: The Four Rounds That Matter

18 min read

A React interview asks four different kinds of question and grades each one differently. A concept check measures the precision of a verbal answer, a machine coding round measures whether the component works when the timer stops, a debugging round measures how fast you find the cause in unfamiliar code, and a design round measures your argument rather than your answer. This guide sorts the recurring React questions into those four rounds, gives the follow-up each concept answer invites, and states what a test suite actually asserts for the hooks and components that keep coming back. Versions here are current as of August 2026: React 19.2 is the current release line.

The Question Depends on the Round You're In

Reciting how the virtual DOM works earns nothing in a build round where the only measurement is whether your typeahead resets its timer on the next keystroke and clears it on unmount.

That gap is where preparation usually goes wrong. Reading answers prepares you well for one round out of four. A written answer trains recall, while a build round tests how fast you can turn a spec into working code, a debugging round tests how fast you locate a cause in code you have never seen, and a design round tests whether you can defend a choice against the tradeoff the interviewer raises immediately after you make it.

Four rounds, defined once, then the questions that recur in each: the concept answers that get credit and the trap answers that do not, the hooks and components that come up in build rounds along with what a passing test suite asserts about them, five broken components with the one-line tell that identifies each, and the design questions where the argument is the whole answer.

The Four Rounds React Gets Tested In

Round 1, the concept check. Verbal, short, usually over a screen share, sometimes as a warm-up before code. Graded on precision. The interviewer has a follow-up loaded behind every question, and your first answer decides which follow-up you get.

Round 2, machine coding. You are given a spec and build a component or a hook in an editor. Graded on working behaviour, on the edge cases you handled without being asked, and on whether you finished. Time budgets vary by company and are normally stated up front.

Round 3, debugging or code review. You are handed a component that is broken, or one that works but is wrong, and asked what is happening. Graded on how fast you locate the cause and whether you can explain why your fix works rather than just that it does.

Round 4, design and tradeoffs. No single right answer exists, and anything design-shaped belongs here, component API design included. "Would you make this component controlled or uncontrolled?" is scored on the argument, on whether you name the cost of your own choice before the interviewer does.

The next four sections are those four rounds in that order.

1 · Concept check verbal · graded on precision 2 · Machine coding build it · graded on working behaviour 3 · Debugging graded on how fast you find the cause 4 · Design tradeoffs no right answer · graded on the argument
The four rounds, and what each one is actually graded on.

Round 1: Concept Checks, and the Answers That Get Credit

The warm-up band has short correct answers. JSX is not HTML and never reaches the browser: a build step compiles it into function calls, and with the modern transform those are calls into the react/jsx-runtime package rather than React.createElement. Props are the arguments a parent passes and a component treats as read only; state is a value the component owns. One-way data flow means a child never writes to a parent's state, it calls a function the parent passed down. The class lifecycle maps onto Effects: componentDidMount and componentDidUpdate collapse into one useEffect with the right dependency array, and componentWillUnmount becomes the function that Effect returns. The Rules of Hooks answer is worth memorising verbatim: call hooks at the top level of a React function, never inside conditions, loops or nested functions, so the call order is identical on every render.

Then the five questions that carry a real follow-up.

The virtual DOM. The trap answer is "it's faster than the real DOM." The answer that gets credit describes the mechanism: React renders a description of the UI, compares it against the previous description, and commits only the differences, and it batches updates so a handler calling three setters produces one render. Fewer DOM mutations get committed. The follow-up is "when does that cost you?" A component that re-renders and produces identical output still pays for the render and the comparison.

Reconciliation and keys. Two rules: keys must be unique among siblings, and keys must not change. The docs are blunt that "Index as a key often leads to subtle and confusing bugs", and that key={Math.random()} "will cause keys to never match up between renders, leading to all your components and DOM being recreated every time." The follow-up: "how do you deliberately reset a component's state when a prop changes?" Pass the changing value as the key. React then treats the two instances as different components sharing no state, which is the documented alternative to resetting state in an Effect.

Automatic batching. Before React 18, only updates inside React event handlers were batched; updates inside promises, setTimeout and native event handlers each caused their own render. React 18 batches all of them for roots created with createRoot; legacy ReactDOM.render kept the old behaviour, which is moot from React 19, where that API is removed. Follow-up: "how do you force a synchronous update?" flushSync flushes the updates inside its callback synchronously so the DOM is updated immediately, and the docs call it uncommon and a last resort.

StrictMode. Development-only checks: components render an extra time, Effects run an extra time, ref callbacks run an extra time, deprecated APIs are flagged, and the docs state these checks do not impact the production build. Follow-up: "your Effect subscribes twice in development, do you disable StrictMode?" No. The double run is the test, and the cleanup function is the fix.

memo. It compares each prop with Object.is, which is a shallow comparison.

const user = { id: 1 };
console.log(Object.is(user, user));
console.log(Object.is({ id: 1 }, { id: 1 }));
console.log(Object.is([], []));
true
false
false

A fresh object, array or inline function passed as a prop defeats it every render. The follow-up: "your memo isn't working, why?" A memoized component still re-renders when its own state changes or when a context it reads changes, and the docs are explicit that "memoization is a performance optimization, not a guarantee."

useMemo and useCallback. The answer now starts by asking whether the project compiles. React Compiler memoizes automatically at build time, and it can memoize conditionally, which manual memoization cannot; where it runs, the two Hooks stay available as an escape hatch for controlling by hand which values are memoized. Where it does not, the old rules hold: useMemo for an expensive computation or a referentially stable object, useCallback for a function passed to a memoized child. The follow-up: "so is memoization dead?" No. The compiler is opt-in and installed through a build tool, which is why the question comes first, and the compiler section below has the version detail.

Round 2: Machine Coding, and the Hooks and Components That Recur

The build set is small and it repeats. Four hooks are worth being able to type without thinking. Where an exercise is linked below it opens as a workspace with the tests already written; everything else here is a spec to type from scratch against your own.

useDebounce. The suite runs under fake timers and asserts four things: the callback has not fired before the delay elapses; three rapid calls produce one call carrying the last arguments, because each call clears the pending timer; unmounting before the delay clears the timer so nothing fires afterwards; and changing the delay does not leave the previous timer running. The reset behaviour is the part candidates skip. Here it is in the plain debounce the hook wraps, with the timer inside the closure so each debounced function owns its own:

function debounce(fn, ms) {
  let timer;
  return (...args) => {
    clearTimeout(timer);
    timer = setTimeout(() => fn(...args), ms);
  };
}
const search = debounce((q) => console.log('request for', q), 50);
search('r');
search('re');
search('rea');
console.log('typing finished');
typing finished
request for rea

usePrevious. Returns undefined on the first render, then the value from the render before. The common implementation writes to a ref inside an Effect, which is why the render body still sees the old value. A related pair worth knowing is useLatest and useEventCallback, which solve the opposite problem: a stable function identity that always reads current state.

useFetch. Loading is true before the promise settles, error is populated on rejection, an AbortController is aborted in the cleanup, and the abort rejection is distinguished from a real failure so a cancelled request never sets error state. Add the stale-response guard from round 3. A fuller version of this is the mini React Query core exercise, where caching and refetching enter the spec.

useLocalStorage. A lazy initialiser so storage is read once rather than on every render, a JSON.parse that survives corrupt data, and a write on every change.

The component set is equally predictable: typeahead with keyboard navigation, accordion, tabs, star rating, modal with focus trap, infinite scroll. For the accordion the ARIA pattern is the spec: the header title sits in an element with role button, that button is wrapped in an element with role heading that has a value set for aria-level, and the button carries aria-expanded set to true when its panel is visible and false when it is hidden, and aria-controls pointing at the panel's id. Enter or Space toggles. Whether more than one panel can be open at a time is a question to ask, not to assume. For the typeahead, arrow keys move the active option, Enter commits it, Escape closes the list, and a slow response for an abandoned query must never overwrite a newer one. For the modal, focus moves into the dialog on open, Tab cycles inside it, Escape closes, and focus returns to the trigger.

Round 3: Five Broken Components You Should Recognise on Sight

These five are written to be read and recognised on the page: there is no graded workspace for each of them yet. The drill for this round is in the prep plan below — break your own components these five ways and time how long the diagnosis takes.

1. The counter that freezes at 1. Symptom: an Effect starts a setInterval with an empty dependency array and the display goes 0, 1, and stops. Cause: the callback captured the state value from the first render and keeps reading that same value forever.

function render(count, keep) {
  const tick = () => console.log('tick sees', count);
  return keep ? tick : null;
}
const tick = render(0, true);
render(1, false);
tick();
tick();
tick sees 0
tick sees 0

The tell is setCount(count + 1) inside a callback created in an Effect with []. Fix: the functional updater, setCount(c => c + 1), so the callback never reads the captured value.

2. The infinite render loop. Symptom: the component renders continuously and the tab heats up. Cause: an object or array literal in the dependency array. The literal is a new reference on every render, the Effect re-runs, the Effect sets state, the render produces a new literal. The tell is }, [options]) where options is built inline above. Fix: depend on the primitive fields, hoist the literal outside the component, or memoize it. useWhyDidYouUpdate is the diagnostic version of this bug, printing which prop changed identity.

3. Typed input jumping rows. Symptom: a list of rows with text inputs, you type into row two, delete row one, and your text is now attached to the wrong row. Cause: key={index}. React matches by key, the indices shift, and the input's DOM node is reused in place: your text stays at the position it was typed into while the data behind it moves up a row. The tell is key={i} on a list that can reorder or delete. Fix: a stable id from the data.

4. The fetch race the slow response wins. Symptom: you type fast, results flicker, and the list settles on an earlier query. Cause: the Effect for the abandoned query is still in flight and calls setState when it lands. The documented fix is an ignore flag in the cleanup:

function fetchUser(id, ms) {
  return new Promise((resolve) => setTimeout(() => resolve('user ' + id), ms));
}
function effect(id, ms) {
  let ignore = false;
  fetchUser(id, ms).then((data) => {
    if (!ignore) console.log('setState', data);
  });
  return () => { ignore = true; };
}
const cleanup = effect(1, 40);
cleanup();
effect(2, 10);
setState user 2

All responses except the last requested one are ignored, even though the first one resolves later.

5. State that changes but never renders. Symptom: you push to an array or sort it, the data is correct in the console, and the screen does not move. Cause: mutation leaves the reference identical, so React sees no change.

const items = ['b', 'a'];
const next = items;
next.push('c');
next.sort();
console.log(items === next);
true

The tell is items.push(...) or items.sort() followed by setItems(items). Fix: build a new array, for example setItems([...items, value]) or setItems([...items].sort()).

Round 4: Design and Tradeoffs, Questions With No Right Answer

Controlled or uncontrolled? The answer that scores is "both, and here is the switch." Accept value plus onChange for the controlled path, defaultValue for the uncontrolled path, and decide the mode by whether value is supplied. Then name the cost yourself: a controlled input re-renders the owner on every keystroke, and an uncontrolled one makes the parent unable to reset the field without a key change. useControllableValue is that switch extracted into a hook.

Where does this state live? The ladder is colocate, lift, context, external store, and each rung costs something. Colocated state is cheapest and unreachable from siblings. Lifting makes it shareable and makes the owner re-render for changes it does not use. Context removes prop drilling and re-renders every consumer when the provider value changes, which is why splitting one context into two, memoizing the value, or reading through a selector are all real answers.

cheapest most reach Colocate siblings cannot reach it Lift owner re-renders for changes it never uses Context every consumer re-renders on any change External store useSyncExternalStore · guards against tearing
Each rung buys reach and charges for it. The cost is the answer the interviewer wants.

An external store read through useSyncExternalStore is the last rung. It takes subscribe, getSnapshot and an optional getServerSnapshot, and while the store has not changed, repeated calls to getSnapshot must return the same value; when it changes, the new value must differ by Object.is. The reason it exists is tearing: during a Transition, React can render some components before a store update and some after, so two components show different values from the same store within a single visual update. React guards against this by calling getSnapshot a second time just before applying changes to the DOM and falling back to a blocking update if the store was mutated. Building an observable store or createGlobalState is the practical version of this question.

Suspense or an Effect for data? use, the React 19 API for reading a promise during render, suspends on a pending promise and sends errors to the nearest Error Boundary, which moves loading and error UI out of the component and up the tree. An Effect keeps both inside the component, where the component also owns the race guard. The caveats on use are in the React 19 section below.

Long lists: virtualise or paginate? Virtualisation keeps the whole dataset in memory and renders a window of rows, which costs scroll complexity and breaks naive find-in-page. Pagination keeps the DOM small and the data fetching simple, at the cost of a click. The same shape of argument covers lazy plus Suspense: code splitting trades a smaller first load for a spinner at the moment of interaction.

What React 19 and 19.2 Changed in the Question Set

React 19.0 shipped on 5 December 2024 and moved several answers.

ref is a prop. Function components can read ref from props directly. The release post states that new function components will no longer need forwardRef and that "In future versions we will deprecate and remove forwardRef." It still works today, so treat it as announced future removal rather than a deprecation.

Actions. useActionState takes an action and returns the last result, a wrapped action and a pending flag: const [error, submitAction, isPending] = useActionState(async (previousState, formData) => {...}, null). useFormStatus reads the status of the parent <form> as if the form were a Context provider, returning pending among other fields, so a submit button never needs the prop passed to it. useOptimistic renders an optimistic value while the request is in flight and switches back to the real value when the update finishes or errors.

use. It must be called inside a component or hook, and unlike other hooks it can be called inside loops and conditionals, including after an early return. That is the point of it, so do not describe it as following the Rules of Hooks. It cannot be wrapped in try/catch, because errors belong to the nearest Error Boundary, and the promise must be cached rather than created during render.

Removals worth knowing. propTypes checks are gone and silently ignored, defaultProps is removed from function components while class components keep it, string refs and legacy context are gone, ReactDOM.render and ReactDOM.hydrate are gone, and act moved from react-dom/test-utils into the react package.

React Compiler 1.0, announced 7 October 2025, is a build-time tool that optimizes through automatic memoization, and it can memoize conditionally, which manual memoization cannot. It is opt-in and installed through a build tool, with support for Babel, Vite and Rsbuild, designed to work best with React 19 while also supporting 17 and 18, and its lint rules ship in eslint-plugin-react-hooks. The updated answer to "when do you reach for useMemo?" begins by asking whether the project compiles, because useMemo and useCallback remain as an escape hatch for controlling which values are memoized.

React 19.2, released 1 October 2025, added <Activity /> with visible and hidden modes for pre-rendering and deferring hidden UI, useEffectEvent, cacheSignal, partial pre-rendering APIs, and Performance Tracks in Chrome DevTools profiles with a Scheduler track and a Components track. useEffectEvent is called at the top level of a component or custom hook, can only be called from inside Effects or other Effect Events, must not be passed to other components or used as an event handler prop, and intentionally has an unstable identity, so it must be left out of dependency arrays.

A Prep Plan That Ends With a Green Test Suite

Two weeks, ordered by round. The exercises linked through this guide open as graded workspaces; the wider set, with the tests and explanations attached, is UIReady Premium.

Days 1 to 3: the four hooks. useDebounce, usePrevious, useFetch, useLocalStorage, typed from scratch each time rather than read. They are the shortest path to fluency because between them they exercise a dependency array, a cleanup and a ref, which is most of what a build round needs. Adjacent exercises like useUpdateEffect, useIsMounted and useEffectOnce drill the same muscles in less time.

Days 4 to 9: two components a day against a visible timer, using a budget scaled to the component as the pace check (15 minutes for a star rating, 25 to 40 for an accordion or tabs, 40 to 50 for a typeahead or a modal with a focus trap). Accordion, tabs, star rating, modal, typeahead, infinite scroll. Stop when the timer stops, then read the solution and note only the edge case you missed.

Days 10 to 12: the debugging set. Break your own components deliberately, five ways, and time how long it takes to find each cause.

Days 13 and 14: say the round 1 answers out loud, including the follow-up to each. Silent reading hides the sentences you cannot actually finish.

The finish line is measurable and it is not a feeling of readiness. It is a test suite you did not write going green on the first run, which is what a machine coding round grades you on. Running that whole set is what the lifetime tier is for.

Frequently asked questions

Which React interview questions should I prepare first?
Prepare by round rather than by topic. Concept checks reward precise verbal answers about reconciliation, keys, batching and memo; machine coding rounds reward being able to type useDebounce, useFetch, an accordion or a typeahead from memory; debugging rounds reward recognising a stale closure or an index-as-key bug on sight. The build set is the smallest, and it is the slowest to acquire because typing fluency only comes from typing, so start there.
Do I still need useMemo and useCallback with React Compiler?
React Compiler 1.0, released on 7 October 2025, is a build-time tool that memoizes automatically, and it can memoize conditionally, which manual memoization cannot. React's own guidance is that useMemo and useCallback remain available as an escape hatch to control which values are memoized. The compiler is opt-in and installed through a build tool, so the correct interview answer starts by asking whether the project has it enabled.
Is forwardRef deprecated in React 19?
No. React 19 lets you read ref as a plain prop on function components, and the release post says new function components no longer need forwardRef and that it will be deprecated and removed in future versions. As of React 19.2 it still works, so calling it deprecated today is wrong.
How do I fix a stale closure in useEffect?
Identify which value the Effect captured on its first run. If it is state you are updating, switch to the functional updater form so the callback never reads the captured value. If it is a prop or state the Effect genuinely needs to read but should not re-run on, React 19.2 added useEffectEvent, which always sees the latest props and state and must be kept out of the dependency array.
What does a passing test suite for useDebounce actually check?
Four things, under fake timers: the callback has not fired before the delay elapses; rapid calls reset the timer so only one call lands, carrying the last arguments; unmounting before the delay clears the pending timer so nothing fires afterwards; and changing the delay does not leave the old timer running.