This article is published in English.
Twenty React interview questions that separate usage from understanding
Virtual DOM, keys, effects, memoization, Context, SSR, and hydration—explained with the gotchas interviewers actually probe, not textbook definitions.
Shipping React and explaining React are different skills. Interviews probe the second: what happens on setState, why keys matter, when effects re-run. The twenty questions below show up constantly. Answers lean on the problem each feature solves and the gotchas that bite in production—not textbook recitations.
1. What the Virtual DOM actually is
The Virtual DOM is not mystical acceleration fairy dust. It is a plain JavaScript tree describing what the UI should look like. On state change React builds a new virtual tree, diffs it against the previous one, and reconciles by applying only the necessary patches to the browser DOM. Real DOM work triggers layout and paint; mutating JS objects is cheap, so React spends CPU in memory to stay stingy with browser work. That design is about minimizing browser thrash, not about making every comparison free. Saying “Virtual DOM is always faster” without that nuance is a common junior trap.
2. Virtual DOM versus the browser DOM
Real DOM updates are heavy and can reflow and repaint. Virtual updates are in-memory object diffs; React batches expensive DOM writes instead of paying once per state call. Editing with track-changes beats rewriting the whole document for every typo.
3. Why list keys matter when rows move
Keys identify elements across renders. Without them React falls back to index position, which breaks when items insert, delete, or reorder. Using the array index as a key “works” until reorder leaves inputs and checkboxes attached to the wrong rows—a fake “state leak” that is really a key bug. Prefer stable unique IDs from data; indexes only for truly static lists. If the product allows drag-and-drop reorder or filtering, indexes as keys will eventually corrupt row-local state.
4. Explain useState from first principles
Ordinary locals die when a function returns. useState gives a function component durable memory and schedules a re-render when that memory changes:
const [count, setCount] = useState(0);
count is the current value; setCount requests an update. The update is not applied mid-render: logging count immediately after setCount still shows the previous value because the new value appears on the next render.
5. Why useState updates feel delayed or batched
React batches updates that land in the same event turn into one re-render. Since React 18, that batching covers promises and timers too, not only React event handlers. When you need the latest value based on previous state, use the functional updater:
setCount(prev => prev + 1);
That form reads the freshest queued value instead of a stale closure snapshot. Interviewers often follow up by asking what goes wrong if you close over count inside a timeout without the updater form.
6. What problem does useEffect actually solve?
Render should be a pure function from props and state to JSX. Apps also fetch data, attach event listeners, schedule timers, and touch the DOM—side effects. useEffect runs that impure work after commit, not during render:
useEffect(() => {
const id = setInterval(() => console.log("tick"), 1000);
return () => clearInterval(id); // cleanup
}, []);
The returned cleanup runs before the next effect and on unmount. Skip it and interviews will ask about duplicate timers and leaked listeners that keep firing after unmount.
7. What’s the dependency array actually controlling?
It tells React when to re-run the effect via shallow compares:
[]— once after mount[count]— again whencountchanges- omitted — after every render (rarely desired)
Forget a referenced value in the array and you get a stale closure: the effect keeps the first captured value forever. Exhaustive-deps lint rules exist because this class of bug is so common.
8. Controlled vs uncontrolled components
Controlled inputs take value from React state and update via onChange; React owns the truth.
Uncontrolled inputs keep DOM state; read via ref when needed (often on submit).
// Controlled
<input value={name} onChange={e => setName(e.target.value)} />
// Uncontrolled
<input ref={inputRef} defaultValue="Dev" />
Controlled enables live validation and formatting at the cost of a render per keystroke. Uncontrolled stays lighter when you only need the final value. Mixed forms—some fields controlled, some not—are possible but harder to reason about in reviews.
9. Prop drilling and when to stop
Prop drilling relays data through layers that only forward it. Renames touch many files. Context helps for theme, auth, or locale; Redux or Zustand help when state graphs get complex. Nuance: Context is not free—every consumer re-renders when the value changes—so it is not the default for every shared field. Passing props two levels is often clearer than inventing a context provider for a one-off value.
10. When would you reach for useReducer instead of useState?
Reach for useReducer when updates branch by action type, next state depends on previous state in non-trivial ways, or several fields move together:
function reducer(state, action) {
switch (action.type) {
case "increment": return { count: state.count + 1 };
case "reset": return { count: 0 };
default: return state;
}
}
const [state, dispatch] = useReducer(reducer, { count: 0 });
It is a mini-Redux inside the component. A boolean toggle stays on useState; tangled transition logic belongs in one testable reducer. Moving that logic out of JSX event handlers also makes unit tests trivial without rendering the whole tree.
11. Explain useMemo vs useCallback without just reciting the docs
Both skip redundant work between renders for different shapes of data:
useMemocaches a computed valueuseCallbackcaches a function reference
const sorted = useMemo(() => expensiveSort(list), [list]);
const handleClick = useCallback(() => doThing(id), [id]);
Function identity matters because each render creates a new function object. Passing a fresh function into a memo child defeats that memoization; useCallback keeps the reference stable. Do not sprinkle either hook everywhere—memoization has cost. Use them for measured expensive work or memoized children. Premature memoization is a frequent junior failure mode that interviewers like to poke.
12. React.memo is useful—and easy to defeat
React.memo skips a re-render when props are shallowly equal. New object or array literals count as different even with identical contents, so inline { style: { color: 'red' } } makes memo useless unless parents stabilize props with useMemo/useCallback. Otherwise memo adds comparison cost without preventing child work.
13. Keys as identity, not just a console warning
Keys are React’s identity system between renders. Wrong keys reuse the wrong DOM node for the wrong data: form state sticks to another row, animations fire on the wrong element, list-item useState keeps the previous item’s value. It looks like state corruption; it is a keys bug. Demonstrating a broken index-key list in a sandbox is one of the fastest ways to internalize the rule.
14. Context: right jobs and wrong jobs
Context shares values many components need without prop threading—authenticated user, theme, locale:
const ThemeContext = createContext();
<ThemeContext.Provider value={theme}>
<App />
</ThemeContext.Provider>
It is a poor fit for high-frequency state (per-keystroke form values) across a large tree, because every consumer updates on every change with no selective subscription. Prefer a store with finer subscriptions for that shape. Theme toggles are a classic good Context fit; cursor positions in a collaborative editor usually are not.
15. Mapping class lifecycles onto effects
Rough class mapping:
componentDidMount→useEffect(..., [])componentDidUpdate→useEffect(..., [dep])componentWillUnmount→ effect cleanup
The deeper shift: lifecycles think in time (mount/update/unmount); effects think in synchronization—keep this external system aligned with these values—which is why effects re-run when dependencies change. Treating effects as lifecycle methods translated line-for-line is how people end up fighting the dependency array.
16. State compared with props
Props are read-only inputs from a parent. State is owned data that triggers re-renders when it changes. Props configure a component from outside; state is what it remembers about itself. A button’s label is a prop; whether it is disabled while a request is in flight is state. Confusing the two leads to anti-patterns like trying to mutate props or lifting ephemeral UI flags too high.
17. Finding unnecessary re-renders without guessing
Common causes: parents passing new object/array/function literals, Context churn re-rendering all consumers, or state living higher than necessary. Do not guess—use the React DevTools Profiler, record an interaction, and read which props changed. Fixes often move state down or split components so expensive subtrees stop riding cheap updates, rather than blanketing useMemo first. Profilers turn “it feels slow” into a concrete parent/prop explanation you can fix.
18. SSR compared with CSR
CSR ships a thin HTML shell plus JS; the browser builds the page after download—fast to serve, slower to meaningful paint, historically weaker for SEO until JS runs. SSR sends HTML per request, then hydrates by attaching listeners—better first paint and SEO, more server work. Next.js and friends add static generation and streaming, but the core trade-off remains TTFB/SEO versus server cost and complexity. Saying “SSR is always better” without naming that trade-off is a weak interview answer.
19. Hydration mismatches and how they happen
Hydration attaches React to server HTML without throwing the markup away. Mismatches happen when server HTML differs from the client’s first render: Date.now() or Math.random() in render, window checks that diverge on the server, or extensions injecting nodes. React warns loudly and frequently re-renders on the client to recover—extra work plus a visible flash of incorrect content. Guarding browser-only APIs behind useEffect or feature components is the usual preventive pattern.
20. Why React wraps native events in SyntheticEvent
React’s SyntheticEvent normalizes cross-browser quirks (so onChange behaves consistently) and historically used root-level event delegation instead of one native listener per node. React 17+ delegates to the app root container rather than document, but the idea remains. Pooling used to reuse event objects so async access could see null; pooling is gone since React 17, yet knowing the layer between browser event and handler still signals depth. Mentioning that e.nativeEvent still exists underneath shows you know the abstraction is a wrapper, not a replacement for the DOM event model.
What interviews actually reward
Strong interview answers explain the problem, the gotcha, and how you would unblock a teammate—not a memorized definition. Interviewers are less interested in whether you can define useEffect and more interested in whether stale closures, missing cleanup, or dependency mistakes have bitten you and you can say why. Reproduce those bugs in a sandbox before the interview; lived scars read as understanding. Definitions get you past the first minute; trade-offs and failure stories carry the rest of the conversation.
Keep a short personal cheatsheet of bugs you have fixed—stale effects, bad keys, hydration mismatches—and practice explaining each in under a minute. That preparation beats cramming API signatures the night before, and it gives concrete stories when an interviewer asks for an example from your work. Pair each story with the fix you shipped so the answer ends on judgment, not only on pain. That closing move is what separates “read the docs” from “operated this stack under pressure.”