Most "React interview questions" lists are answer keys — a question, three sentences, next. That's fine for a five-minute refresher, but it's useless for actually understanding why the answer is the answer. This is my attempt at the other version: the same territory a typical React interview bank covers (I used the well-known Devinterview.io React question set as a map of what to include), rewritten in my own words, with the reasoning spelled out, real code, and a few diagrams for the parts that are easier to see than to read. Organized by topic, so skip to what you need.
Core concepts
Q1. What is the Virtual DOM, and why does React bother with one?
The real DOM is expensive to touch — every mutation can trigger layout recalculation, style recomputation, and repainting. React's answer is to keep a lightweight, plain-JavaScript description of the UI (the "virtual" DOM) and do all the expensive comparison work there first. When state changes, React builds a new virtual tree, diffs it against the previous one, and only then applies the minimal set of real DOM operations needed to reconcile the difference. You're trading cheap JS-object comparisons for expensive DOM operations, which is a good trade almost everywhere in a UI.
What happens between a state update and a repaint
JSX
Compiled to React.createElement calls
Virtual DOM
New element tree, plain JS objects
Reconciler
Diffs new tree vs. previous tree
Real DOM
Minimal patch, then the browser paints
Q2. JSX vs. React.createElement — what is JSX actually compiling to?
JSX isn't HTML and it isn't magic — it's syntactic sugar that a compiler (Babel, or the Next.js/SWC toolchain) turns into plain function calls before your code ever runs in the browser. Newer React versions compile JSX to a jsx() runtime call instead of createElement directly, but the mental model is identical: JSX is just a nicer way to write a tree of function calls that each describe one element.
// What you write:
function Welcome({ name }) {
return <h1 className="title">Hello, {name}</h1>;
}
// Roughly what the compiler produces:
function Welcome({ name }) {
return React.createElement(
"h1",
{ className: "title" },
"Hello, ",
name,
);
}That's also why a component's name must start with a capital letter — createElement("div", ...) treats a lowercase tag as a plain HTML tag string, while an uppercase identifier is treated as a reference to a function/class to call.
Q3. Why do list items need a stable key, and why is array index a bad one?
The diffing algorithm compares old and new children lists positionally by default. A key tells React "this specific item persisted across renders, even if its position changed" — without it (or with an unstable one like the array index), React can't tell a reorder from a delete-and-recreate, so it may unmount/remount DOM nodes it didn't need to, losing input focus, animation state, or uncontrolled form values in the process.
// Fragile: if the list is reordered or filtered, index-as-key makes
// React match the WRONG previous element to each new position.
todos.map((todo, i) => <TodoRow key={i} todo={todo} />);
// Correct: key follows the actual entity, wherever it ends up.
todos.map((todo) => <TodoRow key={todo.id} todo={todo} />);- Index keys are only safe for lists that are static — never reordered, filtered, or have items inserted/removed.
- A key only needs to be unique among siblings, not globally.
- Keys aren't passed down as a prop — if the child needs the id too, pass it explicitly.
Components, props & state
Q4. Function components vs. class components — is there still a reason to reach for a class?
Practically, no — for new code. Hooks (introduced in React 16.8) gave function components everything classes could do, with less boilerplate and no this-binding footguns. The one real gap: there is still no hook equivalent for componentDidCatch/getDerivedStateFromError, so error boundaries remain classes. Everything else below is why function + hooks won by default.
| Function + Hooks | Class | |
|---|---|---|
| State | useState / useReducer | this.state + this.setState |
| Side effects | useEffect (one API, all phases) | 3 separate lifecycle methods |
| Sharing logic | Custom hooks — compose freely | HOCs / render props — wrapper hell |
| this binding | Not a concern | Common source of bugs |
| Error boundaries | Not supported | Only option (componentDidCatch) |
Q5. Props vs. state — what's the actual line between them?
Props are how a parent configures a child — they flow one way, down, and a component must never mutate its own props. State is data a component owns and manages itself, and changing it (via setState/useState's setter) is what triggers a re-render. A useful test: if the value is handed to you from outside, it's a prop; if you're the one deciding when it changes, it's state. A very common bug is copying a prop into state with useState(prop) and expecting it to stay in sync — it won't, because the initial value is only read once.
Q6. Controlled vs. uncontrolled inputs
| Controlled | Uncontrolled | |
|---|---|---|
| Source of truth | React state (value + onChange) | The DOM itself |
| Reading the value | Always available in state | Pull it on demand via a ref |
| Validation as you type | Trivial | Needs manual event wiring |
| Typical use | Forms with live validation/formatting | Simple forms, file inputs, quick prototypes |
// Controlled — React owns the value, every keystroke is a state update.
function ControlledInput() {
const [value, setValue] = useState("");
return <input value={value} onChange={(e) => setValue(e.target.value)} />;
}
// Uncontrolled — the DOM owns the value; React reads it only when asked.
function UncontrolledInput() {
const ref = useRef(null);
const handleSubmit = () => console.log(ref.current.value);
return <input ref={ref} defaultValue="" />;
}Hooks in depth
Q7. useState — why does setCount(count + 1) called twice in a row only add 1, not 2?
Inside a single event handler, count is a constant captured from that render's closure — it doesn't change between the two calls. setCount(count + 1) run twice just tells React "set it to count+1" twice, which is the same instruction repeated, not two increments. The functional-update form fixes this by receiving the latest pending value instead of the stale closed-over one.
// Bug: both calls read the same stale `count` from this render's closure.
setCount(count + 1);
setCount(count + 1); // count is STILL +1, not +2
// Fix: the updater function always receives the latest queued value.
setCount((c) => c + 1);
setCount((c) => c + 1); // now genuinely +2Q8. useEffect — what actually goes wrong with the dependency array?
useEffect re-runs its callback whenever any value in the dependency array changed since the last render (by ===). The two mistakes that show up constantly: omitting a dependency the effect actually reads (stale-data bugs), and putting an object/array/function literal in the array that gets a new reference every render (infinite-loop bugs, since a new reference always counts as "changed"). The cleanup function returned from the effect runs before the next execution and on unmount — it's the one place to undo subscriptions, timers, or listeners so they don't leak.
useEffect(() => {
const id = setInterval(() => setTick((t) => t + 1), 1000);
// Cleanup — without this, every re-render (or unmount) leaves the old
// interval running in the background, ticking a state setter for a
// component that may no longer exist.
return () => clearInterval(id);
}, []); // empty array = run once on mount, clean up once on unmount- If the effect doesn't read any props/state, [] is correct and safe.
- If it does, list every one of them — the exhaustive-deps ESLint rule exists because "I know better" is usually the bug.
- An effect that only responds to a click shouldn't be an effect at all — put that logic directly in the event handler.
Q9. useMemo vs. useCallback — do you actually need either of them?
Both exist to preserve a stable reference across renders — useMemo memoizes a computed value, useCallback memoizes a function (and is literally useMemo(() => fn, deps) under the hood). Neither one is free: they cost a comparison and a cache slot every render. They earn their keep in exactly two situations — an expensive computation you don't want to redo every render, or a value/function passed as a prop to a React.memo-wrapped child, where a new reference every render would defeat the memoization entirely.
| useMemo | useCallback | |
|---|---|---|
| Memoizes | A computed value | A function reference |
| Typical use | Expensive derived data (sort, filter, aggregate) | Callback passed to a memoized child |
| Equivalent to | — | useMemo(() => fn, deps) |
| Skip it when | The computation is cheap | Nothing downstream depends on referential stability |
const sorted = useMemo(
() => [...items].sort((a, b) => a.price - b.price),
[items],
);
// Stable reference so <ExpensiveChild /> (wrapped in React.memo) doesn't
// re-render just because the parent re-rendered with a fresh arrow function.
const handleSelect = useCallback((id) => setSelectedId(id), []);
return <ExpensiveChild items={sorted} onSelect={handleSelect} />;Q10. useRef — what is it for, beyond grabbing a DOM node?
A ref is a mutable box that survives re-renders without causing one when it changes — the opposite of state. Past DOM access, that makes it the right tool for anything you need to remember across renders but never want to render: a previous value for comparison, a mutable counter, an interval/timeout id, or a flag like "has this already run."
// A classic custom hook: track the PREVIOUS render's value.
function usePrevious(value) {
const ref = useRef();
useEffect(() => {
ref.current = value; // runs AFTER render, so it always lags by one
}, [value]);
return ref.current;
}Q11. The Rules of Hooks — why can't a hook live inside an if statement?
React doesn't associate state with a variable name — it associates it with call order. On every render, useState/useEffect/etc. calls are matched to their stored state by position in a fixed list tied to that component's fiber. Wrap a hook in a condition and that order can change between renders, so React ends up handing component A's state to what used to be component A's second hook, and now might be its first — silent, hard-to-trace corruption, not a crash.
// Breaks the rules: whether useEffect ever runs depends on `loggedIn`,
// so the hook call order shifts between renders.
if (loggedIn) {
useEffect(() => track("login"), []);
}
// Fix: always call the hook; move the condition inside it.
useEffect(() => {
if (loggedIn) track("login");
}, [loggedIn]);Rendering, reconciliation & Fiber
Q12. What triggers a re-render, and what does React actually do during one?
A component re-renders when its own state changes, its parent re-renders (by default — children re-render whenever the parent does, regardless of whether their own props changed), or a context it consumes changes. "Re-render" doesn't mean "the DOM changes" — React always re-runs the function to get a new virtual tree, diffs it, and only touches the real DOM where the diff says something actually differs.
One update cycle, phase by phase
Render phase
Re-run components, build new tree (interruptible)
Reconciliation
Diff new tree against the last committed one
Commit phase
Apply the minimal DOM mutations (not interruptible)
Paint
Browser reflects the change on screen
Q13. What is Fiber, in terms that don't require reading the source?
Fiber is React's internal reimplementation of the render phase as an interruptible unit of work, instead of one big synchronous recursive tree walk. Each element gets a corresponding "fiber" — a JS object holding its type, props, state, and links to its parent/child/sibling. Because the tree is walked as a linked list of these units rather than a call stack, React can pause after any fiber, let the browser handle something more urgent (a keystroke, an animation frame), and resume later. That's what makes features like time-slicing and Suspense possible — rendering can now have priority.
Q14. Why doesn't a variable reflect the new value right after calling setState?
setState (and useState's setter) schedules a re-render — it doesn't mutate anything synchronously. Inside the function that called it, the local state variable is still the constant it was when that render started; the new value only exists once React re-runs the component. This is also why React batches multiple state updates triggered inside the same event handler into a single re-render instead of one per call — it's not a bug to work around, it's the model.
State management beyond one component
Q15. Prop drilling and the Context API — what does Context actually fix, and what does it not?
Prop drilling is passing a value through five components that don't use it just so the sixth one can. Context lets any descendant read a value directly, skipping the drilling. What it doesn't fix: every component consuming a context re-renders whenever that context's value changes, even if the specific field it reads didn't — a single context object holding {user, theme, cart} means a cart update re-renders every component that only cares about theme. The usual mitigation is splitting one big context into several narrow ones, or reaching for a selector-based library once that starts to hurt.
const ThemeContext = createContext("dark");
function App() {
return (
<ThemeContext.Provider value="dark">
<Toolbar />
</ThemeContext.Provider>
);
}
// Any descendant reads it directly — no props passed through Toolbar.
function ThemedButton() {
const theme = useContext(ThemeContext);
return <button className={theme}>Click</button>;
}Q16. When do you reach for Redux/Zustand instead of Context — and how are they different from each other?
Context is built into React and fine for low-frequency, broadly-needed values (theme, locale, the current user). It gets painful for state that changes often and only some consumers care about, because React has no idea which fields a component read — a real state library adds fine-grained subscriptions so a component only re-renders when the specific slice it selected actually changes. Redux is explicit and unidirectional by design: an action describes what happened, a pure reducer decides the next state, and the store notifies subscribers — which also makes it very debuggable (every state change is a recorded, replayable action). Zustand keeps the selector-subscription model but drops the ceremony — no actions/reducers required, just a store and a hook.
The Redux/Flux unidirectional data flow
Action
Plain object describing what happened
Reducer
Pure function: (state, action) → new state
Store
Holds state, notifies subscribers
UI
Re-renders only the components that selected the changed slice
| Context | Redux | Zustand | |
|---|---|---|---|
| Renders on change | Every consumer | Only components that selected the changed slice | Only components that selected the changed slice |
| Boilerplate | Minimal | Actions + reducers (less with Redux Toolkit) | Minimal — a store + a hook |
| DevTools / time-travel | No | Excellent | Available via middleware |
| Good fit | Low-frequency, widely-needed values | Large apps, complex state transitions, strict traceability | Small-to-mid apps wanting Redux-like selectors without the ceremony |
Performance optimization
Q17. What does React.memo actually skip, and how does it fail silently?
React.memo wraps a component so React can shallow-compare its new props against the previous render's props and skip re-rendering (and re-diffing its subtree) if they're all ===. It fails the moment the parent passes a new object, array, or function literal as a prop on every render — a brand-new reference is never === to the old one, so the shallow comparison always reports "changed" and the memoization does nothing. That's why React.memo on a child is almost always paired with useMemo/useCallback in the parent for exactly the props being passed down.
const Row = React.memo(function Row({ item, onSelect }) {
return <li onClick={() => onSelect(item.id)}>{item.label}</li>;
});
// Without useCallback here, `onSelect` is a new function every render of
// the parent, so React.memo on <Row> never actually skips anything.
const handleSelect = useCallback((id) => setSelectedId(id), []);Q18. How does code-splitting with lazy + Suspense actually reduce load time?
React.lazy turns a component import into a dynamic import() — instead of one giant JS bundle shipped up front, that component's code is fetched in a separate chunk only when it's about to render. Suspense is what lets you declare a fallback (a spinner, a skeleton) to show while that chunk is still downloading, instead of the render just... not happening. The win is entirely about initial bundle size: a route or a rarely-used modal doesn't need to be part of the first paint.
const ReportBuilder = lazy(() => import("./ReportBuilder"));
function Dashboard() {
return (
<Suspense fallback={<Spinner />}>
<ReportBuilder />
</Suspense>
);
}Q19. Beyond memo — what actually causes most unnecessary re-renders in real apps?
- State placed too high in the tree — a keystroke in one input re-rendering an entire page instead of just that input's subtree.
- Context values that change often, consumed by components that only need a small slice of them.
- New object/array/function literals passed as props every render, defeating any memoization downstream.
- Deriving state with useEffect + useState instead of just computing it inline during render — an extra render cycle for data you already had.
Lifecycle methods → Hooks cheat-sheet
For anyone translating between class-based mental models and hooks — or reading a codebase that still has both.
| Class lifecycle | Hook equivalent | Notes |
|---|---|---|
| componentDidMount | useEffect(fn, []) | Runs once, after the first render |
| componentDidUpdate | useEffect(fn, [deps]) | Runs after renders where a listed dependency changed |
| componentWillUnmount | The function useEffect returns | Cleanup — same effect call, not a separate hook |
| shouldComponentUpdate | React.memo / useMemo | Skip re-render or re-computation on unchanged input |
| componentDidCatch / getDerivedStateFromError | No hook equivalent | Error boundaries still require a class |
Patterns, gotchas & good habits
Q20. Why do error boundaries have to be class components?
Catching a render error requires componentDidCatch and/or getDerivedStateFromError — there is currently no hook that lets a function component catch an error thrown by its children during render. If you need one today, you write (or install) a small class wrapper once and use it everywhere else as a regular component; it doesn't need hooks itself to be useful in an otherwise all-hooks codebase.
class ErrorBoundary extends React.Component {
state = { hasError: false };
static getDerivedStateFromError() {
return { hasError: true };
}
componentDidCatch(error, info) {
logToMonitoring(error, info);
}
render() {
if (this.state.hasError) return this.props.fallback;
return this.props.children;
}
}Q21. Synthetic events — why does e.target sometimes not behave like the raw DOM event?
React wraps native DOM events in a SyntheticEvent that normalizes behavior across browsers and integrates with React's own event delegation (React attaches most listeners once at the root and dispatches through its own tree, rather than one native listener per element). Properties like target work the same in practice, but subtleties like event pooling (recycling event objects for performance) existed in older React versions, which is why you'll still see e.persist() in some legacy code — modern React no longer pools events, so it's a non-issue today but worth recognizing if you read older codebases.
- Never mutate state directly (state.items.push(x)) — always create a new reference so React (and memo) can detect the change.
- Derive values during render instead of syncing them into state with an effect — if it can be computed from existing props/state, it doesn't need its own useState.
- Colocate state as close as possible to where it's used; lift it only when a sibling genuinely needs to share it.
- Keep components pure with respect to props/state — no side effects during render, only inside effects or event handlers.
Refs, forwarding & imperative APIs
Q22. What problem does forwardRef actually solve?
A ref passed to a function component doesn't attach to anything by default — function components have no instance for React to point the ref at, so React just warns and gives you nothing. forwardRef lets a component opt in: it receives the ref as a second argument and decides what it points to, usually the underlying DOM node of an input or button it wraps.
const FancyInput = forwardRef(function FancyInput(props, ref) {
return <input ref={ref} className="fancy" {...props} />;
});
// Now a parent can focus the actual <input> through the wrapper:
const inputRef = useRef(null);
<FancyInput ref={inputRef} />;
inputRef.current.focus();Q23. When would you reach for useImperativeHandle instead of just forwarding the ref?
Forwarding the raw DOM node works, but it exposes everything about that node — every property and method, whether you meant to or not. useImperativeHandle lets a component hand back a custom, deliberately small object instead: only the methods you actually want callers to use (focus(), reset(), scrollToTop()), keeping the rest of the implementation free to change.
const VideoPlayer = forwardRef(function VideoPlayer(props, ref) {
const videoRef = useRef(null);
useImperativeHandle(ref, () => ({
play: () => videoRef.current.play(),
pause: () => videoRef.current.pause(),
// Note: NOT exposing videoRef.current itself — callers only get
// these two methods, not the whole <video> element.
}));
return <video ref={videoRef} {...props} />;
});Advanced component patterns
Q24. Higher-Order Components — what are they, and why did hooks make them less common?
A HOC is a function that takes a component and returns a new one with extra behavior or props wired in — react-redux's connect() and withRouter are the classic examples. They work, but stacking several HOCs means the tree in devtools fills with wrapper components, and each one can introduce prop naming collisions since the extra props all merge into the same namespace. A custom hook gives you the same reusable logic without adding a component layer — the caller just destructures whatever it needs.
// HOC: adds a `loading` prop by wrapping the component in a new one.
function withLoading(Component) {
return function Wrapped(props) {
if (props.loading) return <Spinner />;
return <Component {...props} />;
};
}
// Hook equivalent: no extra component, no extra prop-name collisions.
function useLoading(fetcher) {
const [loading, setLoading] = useState(true);
useEffect(() => { fetcher().finally(() => setLoading(false)); }, [fetcher]);
return loading;
}Q25. Render props — what did they solve, and what mostly replaced them?
A render prop is a function passed as a prop that a component calls with some internal state, letting the caller decide exactly what to render with it — a <MouseTracker render={({x, y}) => ...} /> that tracks cursor position but leaves the visual up to you. It's a clever way to share stateful logic without a HOC's wrapper, but it nests visually (render props inside render props gets hard to read) and, like HOCs, has been mostly superseded by custom hooks, which share the same logic flatter and without wrapping JSX around JSX.
Q26. Fragments — why <>...</> instead of just wrapping in a div?
A component can only return one root node, and <>...</> (or the explicit <React.Fragment>) satisfies that without adding a real DOM element. That matters more than it sounds: an extra wrapping <div> can break a CSS grid/flex layout that assumes its direct children are the actual items, adds noise to the accessibility tree, and is one more node the browser has to lay out for no visual reason. Reach for the explicit <React.Fragment key={id}> form specifically when you're returning fragments from a list — the shorthand <> can't take a key.
Q27. Portals — what's an actual case where you need one?
createPortal renders a subtree into a different DOM node than its parent, while keeping it in the same place in the React tree — events still bubble up through React's tree as if nothing moved, and context still flows normally. The classic case is a modal or tooltip: if it renders inside a parent with overflow:hidden or a low z-index, no amount of CSS fixes that without escaping the DOM hierarchy entirely.
function Modal({ children }) {
return createPortal(
<div className="modal-backdrop">{children}</div>,
document.getElementById("modal-root"), // outside the app's normal tree
);
}React 18/19 concurrent features
Q28. What is automatic batching, and how is React 18 different from before?
Before React 18, React only batched multiple setState calls into one re-render when they happened inside a React event handler — the same calls made inside a setTimeout, a Promise callback, or a native event listener each triggered their own separate render. React 18 batches automatically no matter where the update originates, which usually just makes apps faster for free, with zero code changes required.
setTimeout(() => {
setCount((c) => c + 1);
setFlag((f) => !f);
// React < 18: two renders (each setState flushed separately).
// React 18+: one render — both updates batched automatically.
}, 1000);Q29. useTransition — what does marking an update as a "transition" actually change?
Wrapping a state update in startTransition tells React "this one can wait" — React keeps the UI responsive to more urgent updates (a keystroke, a hover) and renders the transition's work at lower priority, interrupting it if something more urgent comes in. isPending flips to true while that background render is in flight, which is exactly enough signal to show a subtle loading affordance instead of freezing the interface.
const [isPending, startTransition] = useTransition();
const [query, setQuery] = useState("");
function handleChange(e) {
setQuery(e.target.value); // urgent — the input must feel instant
startTransition(() => {
setResults(expensiveFilter(e.target.value)); // can lag a beat
});
}Q30. useDeferredValue — how is it actually different from debouncing?
A debounce delays reacting to input until typing pauses for a fixed timer — the expensive work waits on the clock, not on the device. useDeferredValue instead lets the input update instantly while handing rendering a value that trails slightly behind, and React can interrupt that lagging render the moment newer input arrives. The practical difference: on a fast device the deferred value catches up almost immediately; on a slow one it naturally lags more — it adapts to real rendering cost instead of a number you guessed in milliseconds.
Q31. useId — why not just roll your own random id?
Ids generated with Math.random() or a module-level counter can come out different on the server render and the client's first render, which breaks hydration (React expects the markup to match). useId produces an id that's stable and consistent across both. The everyday use is wiring an accessible label to its input without hand-managing unique ids everywhere that component happens to render.
function LabeledInput({ label }) {
const id = useId(); // stable across server + client, unique per instance
return (
<>
<label htmlFor={id}>{label}</label>
<input id={id} />
</>
);
}Q32. useSyncExternalStore — what problem is this the escape hatch for?
It's the hook most state-management libraries (Redux, Zustand, Jotai) use internally to subscribe a component to a store that lives outside React. A naive useState + useEffect subscription can read an inconsistent ("torn") snapshot of that external store if a concurrent render gets interrupted partway through — useSyncExternalStore guarantees the snapshot stays consistent even then. Most application code never calls it directly; it's the primitive library authors build the React bindings on top of.
Strict Mode, testing & a few more gotchas
Q33. Why does StrictMode call some functions twice in development?
In development only, StrictMode deliberately double-invokes component render and double-fires effect mount/unmount/mount cycles. The point is to surface impure logic on purpose — a render that mutates something outside itself, or an effect missing its cleanup — as an obvious dev-time bug, before it becomes an intermittent production issue under real concurrent rendering (which can genuinely re-run renders and remount things). None of this doubling happens in a production build.
Q34. useLayoutEffect vs. useEffect — when does the timing difference actually matter?
| useEffect | useLayoutEffect | |
|---|---|---|
| Runs | After the browser has painted | After DOM mutations, before paint |
| Blocks the next paint | No | Yes |
| Default choice | Almost always | Rarely |
| Good fit | Data fetching, subscriptions, logging | Measuring/adjusting layout before the user sees a flicker |
useEffect is the right default for almost everything — it doesn't hold up the browser. Reach for useLayoutEffect only when skipping straight to useEffect would let the user see a visible flash of the wrong layout: measuring an element's size to position a tooltip, or adjusting scroll position before the frame is shown.
Q35. React.PureComponent vs. React.memo — same idea, different eras?
Essentially yes. PureComponent is the class-era version: it implements shouldComponentUpdate with a shallow comparison of props and state so a class component skips re-rendering when neither changed. React.memo does the same thing for a function component, and additionally accepts an optional custom comparison function as a second argument for the cases where shallow equality isn't quite the right check.
Q36. What did React Testing Library change about how people test components?
It pushed the default away from testing implementation details — a component's internal state, calling its instance methods directly — toward testing what a user actually experiences: querying the rendered output by visible text, label, or ARIA role, and firing real events at it. The payoff is tests that survive a refactor which doesn't change behavior, and that fail exactly when something a user would notice actually breaks — instead of failing on an internal rename that never touched the screen.
Almost every "weird React bug" traces back to one of three things: a stale closure, an unstable reference, or state that should have been derived instead of stored. Learn to spot those three shapes and most of the rest is just syntax.
That's the full tour — from JSX compiling down to function calls, through every hook that actually shows up in interviews, to the concurrent-rendering features that are still new enough that plenty of working React developers haven't touched them yet. If you're prepping for an interview, don't just memorize the answers here; open a scratch file and reproduce a couple of the buggy examples yourself. The stale-closure counter and the index-key list are both five minutes of code and worth more than reading them twice.
Test yourself
A quick mock test covering everything above — one question per major topic. It locks your answer in on the first click (right or wrong, immediately) rather than waiting for a "submit" at the end, since the point is to find the gaps while the explanation is still fresh, not to simulate exam pressure.
Question 1 of 26
Score: 0 / 0
1. Why does React use a virtual DOM before touching the real one?