Virtual DOM, reconciliation and diffing
This is the topic that separates someone who uses React from someone who can explain it. The trap is that the popular one-line answer - "React keeps a copy of the DOM and updates only what changed, which is faster" - is half wrong and gets punished by exactly one follow-up: faster than what? Keeping a second tree and diffing it is strictly more work than a hand-written targeted update. What React sells is not speed; it is that you get to write the destination instead of the journey, and the diff is the price it pays to make that affordable.
The pipeline, in the words to use
There are three phases and they have names. Render: React calls your components and gets back a tree of plain JavaScript objects describing what the UI should look like. Reconcile (the diff): React walks the new tree against the previous one and works out the minimum set of DOM operations that turns one into the other. Commit: React applies those operations to the real DOM, then runs layout effects, then paints, then runs passive effects.
Say "render" precisely, because the word does double duty and interviewers listen for it. In React, a render is a function call - your component runs and returns elements. It is not a paint, and it does not imply the DOM was touched. A component can render fifty times and produce zero DOM mutations, and that is the normal case for a component whose output did not change.
Everything in the render phase must be pure and interruptible: React reserves the right to call your component, throw the result away, and call it again. That is not a hypothetical - Strict Mode does it on purpose in development, and concurrent rendering does it in production when a higher-priority update arrives. The commit phase is where side effects are allowed, and it is synchronous and uninterruptible.
- Render phase
- Your component functions run. Output is React elements - plain objects. No DOM is read or written. Can be paused, restarted, or discarded. Must be pure: no mutation of anything outside the function, no subscriptions, no fetches.
- Reconciliation
- The diff between the element tree just produced and the one from the previous commit. Produces a list of effects - insert this node, update this attribute, delete this subtree. Still no DOM writes.
- Commit phase
- React applies the mutations to the real DOM in one synchronous pass, then runs useLayoutEffect and class componentDidMount/componentDidUpdate before the browser paints, then lets the browser paint, then flushes useEffect.
The diffing algorithm is two assumptions and nothing clever
A general algorithm for finding the minimum edit script between two trees of n nodes is O(n^3). At a thousand nodes that is a billion comparisons per update, which is not a thing you can do sixty times a second. React does not use it. It uses a heuristic pass that is O(n) - one walk of the tree - by assuming two things that are almost always true of real UIs.
- Heuristic one: a different type produces a different tree
- If the element at a position changed type - a div became a span, or Profile became Settings - React does not diff the children. It tears the old subtree out, unmounts it (running cleanups), and builds the new one from scratch. State inside the old subtree is destroyed. This is why the type of the element at a position is the single most important thing about it.
- Heuristic two: the developer can tell React which children are stable across renders, with a key
- Without keys, React pairs children by position: the first old child against the first new one, and so on. With keys, it pairs by key regardless of position, so a reorder is a move rather than a rewrite of every row. React cannot infer identity from content, so the key is you supplying the one fact the algorithm cannot derive.
- Same type, same position
- React keeps the DOM node and the component instance, updates only the attributes that actually differ, and recurses into the children. Hook state survives. This is the path you want almost all of the time.
Questions
Answer each one yourself before you open it.
01Why does React exist? What was wrong with updating the DOM by hand?
Hand-written DOM code makes you specify every transition - for every possible before-state, the exact sequence of operations that reaches the after-state. React lets you write only the after-state, as a function of your data, and works out the transition for you.
The number of transitions grows combinatorially with the number of states, so the bug is never in rendering the page fresh - it is in the fourteenth path that leaves a stale class name or a detached listener. Declarative rendering collapses that surface: there is one description of the UI for a given state, so a state you have not thought about still renders correctly. The virtual DOM exists to make that affordable, not to be fast in itself.
Follow-up So it is about correctness, not performance?
Mostly, yes. React's claim is that it is fast enough that you never have to hand-optimise the common case - not that it beats a hand-written targeted update, which it cannot.
02Imperative versus declarative - give me the one-sentence difference.
Imperative code says how to get there step by step; declarative code says what it should look like and lets something else find the steps.
Concretely: imperative is querySelector, then classList.add, then textContent = total, then remove the old row. Declarative is a function from state to markup, and you never mention the previous state at all. The tell that you have slipped back into imperative inside React is reaching for a ref or a direct DOM query to change something you could have derived from state.
03What is JSX?
Syntax sugar for function calls. Every JSX tag compiles to a call that returns a plain JavaScript object describing an element - historically React.createElement, and since the new transform, jsx() from react/jsx-runtime.
It is not a template language and it is not HTML in your JavaScript. Because it compiles to expressions, JSX obeys ordinary JavaScript rules: you can assign an element to a variable, put it in an array, return it from a function, pass it as a prop. That is also why the syntax quirks are what they are - className instead of class because class is a reserved word, and one root element because a function can only return one value.
Follow-up Is JSX required to use React?
No. You can call createElement directly. Nobody does, because nesting it four levels deep is unreadable, but it is worth being able to say what your JSX compiles to.
04The browser cannot run JSX. What handles it?
A compiler - Babel, or the transform built into esbuild, SWC, Vite or the TypeScript compiler. It runs at build time and its output is ordinary JavaScript function calls.
Nothing about JSX survives to runtime. This matters in an interview because it explains why an unknown lowercase tag becomes a string and a capitalised one becomes a reference: the compiler decides on the case of the first character, at compile time, with no knowledge of what exists.
05Why must a component name start with a capital letter?
Because the JSX transform turns a lowercase tag into a string - the literal "div" - and a capitalised tag into a reference to the identifier of that name. A lowercase component name compiles to the string "myComponent", which React tries to create as an HTML element.
It fails in a confusing way rather than throwing: you get an unknown element in the DOM, no props applied, and no error text naming the cause. The same rule explains why a component reached through an object needs dot notation - <ui.Button /> works because the compiler treats a member expression as a reference too.
06What does a React element actually look like?
A plain object with a type, a props object, and a key. Roughly { type: "div", key: null, props: { className: "row", children: [...] } }. No methods, no DOM node, nothing live.
This is the whole answer to "what is the virtual DOM" - it is that object, and a tree of them. It is cheap to create, cheap to throw away, and can be produced without touching the document, which is what makes it possible to render on a server or in a worker.
Follow-up Is it the same as the DOM node it eventually produces?
No, and it is one-way. The element describes the node; React creates and owns the node. There is no reference from the element to a DOM node - that is what refs are for.
07What is the virtual DOM?
An in-memory tree of plain JavaScript objects describing what the UI should be. React keeps the last one it committed, builds a new one on every render, diffs them, and applies only the difference to the real DOM.
It is not a browser feature, it is not a standard, and it is not a partial copy of the real DOM - no DOM nodes appear in it. The point is that creating and discarding objects is cheap while reading and writing the real DOM is expensive, so React does all its exploratory work in the cheap medium and touches the expensive one once, in a batch.
08Is the virtual DOM the same as the shadow DOM?
No relation at all beyond the word. The shadow DOM is a real browser feature for encapsulating a subtree and its styles inside a custom element. The virtual DOM is a React implementation detail made of ordinary objects.
The names collide and interviewers ask it precisely because the collision catches people. Say the shadow DOM is a web-components API in the browser, the virtual DOM is a library technique in memory, and the browser has never heard of the second one.
09Doesn't keeping a second copy of the tree and diffing it make React slower than direct DOM updates?
Yes - a hand-written targeted update is always faster than render plus diff plus commit, and React has never claimed otherwise. What React buys is that the diff is fast enough to be invisible while the code stays declarative, so you never have to write the targeted update in the first place.
The expensive part of DOM work is not calling a setter, it is layout and paint, and it is reading a layout property in the middle of a batch of writes, which forces a synchronous reflow. React's batching means the DOM is mutated once per commit rather than once per state change, which is often faster in practice than naive hand-written code that writes in a loop. Compared with careful hand-written code, React loses on raw speed and wins on everything else.
Follow-up So when does the virtual DOM cost actually show up?
When the component tree is large and re-renders often - a big list re-rendering on every keystroke. The fix is to render fewer components, not to abandon the model: memoise, lift state down, or virtualise the list.
10Walk me through what happens after I call setState.
React marks the component as needing an update and schedules work. On the next tick it re-runs that component and its children to produce a new element tree, diffs it against the last committed one, produces a list of DOM operations, applies them in one synchronous commit, then runs layout effects, lets the browser paint, and flushes passive effects.
Two details make this answer sound experienced. First, the update is scheduled, not immediate - several setState calls in the same tick collapse into one render. Second, the diff can produce an empty list of operations, in which case the component rendered and the DOM was never touched.
11What does "render" mean in React?
React calls your component function and gets elements back. That is all. It is not a paint, and it does not mean the DOM changed.
Getting this word right is most of the vocabulary battle in a React interview. A parent re-render always re-renders its children by default, but almost none of those renders produce DOM work, because the diff finds nothing different. Someone who says "the child re-rendered" and means "the child's DOM was rebuilt" gives away that they have the wrong model.
12So a re-render does not necessarily touch the DOM?
Correct. A re-render produces a new element tree; the DOM is touched only where the diff finds a difference. Rendering a component that returns the same output does zero DOM work.
This is why chasing re-render counts in the profiler is often wasted effort. A hundred components re-rendering and producing identical output costs you a hundred function calls and a tree walk - usually under a millisecond. It becomes a problem when the render itself is expensive, or when the output really does differ across a large tree.
13Does React re-render the whole DOM when state changes?
No, and it never has. It re-renders components - calls the functions again - starting from the one whose state changed, and then writes only the specific DOM mutations the diff found.
The misconception usually comes from the phrase "React re-renders on every state change", which is true of components and false of the DOM. Say the correction with the boundary in it: state change re-renders that component and its subtree, not the app; and re-rendering a subtree does not rewrite that subtree's DOM.
14What is reconciliation?
The algorithm that decides what to do with the new element tree - which existing DOM nodes and component instances to keep, which to update, which to move, and which to destroy.
The name refers to reconciling two trees, and the outcome that matters is not the DOM operations but the identity decisions: keeping a component instance means keeping its state and skipping its cleanup functions. Destroying one runs every cleanup in the subtree and loses all of its state. Almost every mysterious React bug about state resetting is a reconciliation decision you did not intend.
15Why is a general tree diff O(n^3), and how does React get to O(n)?
The general minimum-edit-distance problem between two trees is cubic in the number of nodes, which is unusable at UI scale. React gives up on finding the minimum and uses two heuristics that make one linear pass enough.
The heuristics are: elements of different types produce different trees, so stop diffing and replace; and children in a list can be matched by a developer-supplied key rather than inferred. Both are assumptions, not theorems - they can be wrong, and when they are wrong you get the classic bugs: state lost because the type changed, and the wrong row updated because keys were indexes.
16What happens if the element type at a position changes between renders?
React tears down the old subtree entirely - unmounts the components, runs their cleanups, destroys their state and their DOM nodes - and mounts the new one from scratch.
This is the source of the two most common state-loss bugs. Rendering {isEditing ? <Editor /> : <Viewer />} destroys everything on each toggle, which may be exactly what you want. Defining a component inside another component recreates the function identity every render, so the type changes every single time and the child remounts on every parent update.
Follow-up And if the type is the same but the props changed?
React keeps the DOM node and the instance, updates only the attributes that differ, and recurses into the children. State survives.
17What are keys for?
They tell React which item in a list is which across renders, so it can match items by identity instead of by position.
Without keys, React pairs the first old child with the first new child. If you insert at the front, every position now holds different data, so React updates every row - and any DOM or component state held in those rows, like an input's value or a CSS transition, is now attached to the wrong item. With stable keys it recognises the shift as one insertion.
Follow-up Do keys have to be globally unique?
No - only unique among siblings. React matches keys within a single list, so two different lists can both use key 1 with no conflict.
18What actually breaks if I use the array index as a key?
Nothing, until the list is reordered, filtered, or an item is inserted anywhere except the end. Then the key no longer identifies the item, so React reuses the wrong component instance for the wrong data.
Take a list of three rows, each with an uncontrolled text input, keyed by index. You type into the first row, then insert a new item at the front. The new item takes key 0. React sees key 0 already exists, keeps that DOM node and its typed value, and just changes the text around it - so your typed text is now sitting next to the new item, and every row's state has shifted down by one. The DOM is also doing maximum work: three updates plus one insert, instead of one insert.
Follow-up When is index-as-key fine?
When the list is static, never reordered or filtered, items are never inserted except at the end, and no row holds internal state. That combination is real - a fixed list of labels - but check all four before you rely on it.
19My data has no id. What do I use as a key?
Derive a stable one from the content - a hash or a composed field like the pair of values that make the row unique. If genuinely nothing is stable, index is the honest fallback, and you should know the reorder caveat comes with it.
The requirement is that the same item gets the same key on the next render, not that the key is pretty. Generating one at render time with Math.random or Date.now is the one thing that is always wrong: the key changes every render, so every item is a delete plus an insert, every row remounts, every input loses focus, and you have turned an O(n) diff into a full rebuild.
20Can I put a key on a component that is not in a list?
Yes, and it is a useful technique - changing the key on a single element forces React to unmount and remount it, which is the cleanest way to reset a component's internal state.
The standard case is a form or a detail panel that should start fresh when the selected record changes: give it key={recordId} and every piece of state inside resets automatically, with no effect that watches the prop and calls setState. Reach for it instead of a useEffect that copies props into state.
21What is React Fiber?
The reconciler rewritten in React 16. Instead of one recursive walk of the tree that runs to completion, it represents the work as a linked list of units - one fiber node per component - so React can do a bit, hand control back to the browser, and pick up where it left off.
The old reconciler used the JavaScript call stack, which cannot be paused. Fiber is a stack React owns, which means it can. Once work is interruptible you can also prioritise it, which is what everything labelled "concurrent" is built on. A fiber holds a component's type, its props, its state, its effect list and pointers to its child, sibling and parent.
Follow-up Did Fiber change how the diff works?
No - the two heuristics and the O(n) result are the same. Fiber changed how the work is scheduled, not what the answer is.
22What did Fiber actually buy users?
That a long render can no longer freeze the page. React renders in chunks, yields to the browser between them so it can handle input and paint, and can throw away a render in progress if something more urgent arrives.
Concretely: typing in an input while a large list filters below it. Under the old reconciler the whole tree rendered in one uninterruptible pass and every keystroke was dropped until it finished. With Fiber plus a transition, the keystroke is urgent and interrupts the list render, which restarts with the new value.
23What does startTransition do?
It marks a state update as low priority. React renders it in the background, keeps the old UI visible and interactive while it works, and abandons that render if a more urgent update arrives.
The two-bucket model is the whole idea: urgent updates must land in the next frame - a keystroke, a click, a drag - and everything else is a transition. The typed value goes in a normal setState, the derived filtering goes in startTransition. useTransition is the hook version, which additionally hands you an isPending flag for a spinner.
Follow-up Does it debounce?
No. There is no delay and no timer. The work starts immediately, at lower priority, and gets interrupted rather than postponed. It is also cancellable in a way a debounce never is.
24And useDeferredValue?
The same idea from the other end. You do not control the setState - you take a value you were handed and produce a lagging copy of it, and React re-renders with the new value at low priority.
Use startTransition when you own the update. Use useDeferredValue when you do not - a value arriving as a prop, or one you want to keep in an input as the source of truth while the expensive consumer trails behind. The pattern is: keep query for the input, pass useDeferredValue(query) to the expensive list, and dim the list while the two differ.
25What is Suspense, in one paragraph?
A boundary that catches a child that is not ready yet and renders a fallback until it is. It turns "is this loading" from a piece of state each component tracks into a structural property of the tree.
A component suspends by throwing a promise (or, in modern terms, by reading a resource that has not resolved); the nearest Suspense boundary above it renders its fallback, and swaps back when the promise settles. It started as lazy-loading code with React.lazy, and now covers data too via frameworks and use(). The design win is that the loading state lives at the boundary you chose, so one spinner covers a region instead of six spinners flickering independently.
26What is Strict Mode and why does everything run twice?
A development-only wrapper that deliberately double-invokes renders and re-runs effects - mount, unmount, mount - to surface code that is not pure or not correctly cleaned up. It does nothing in production.
It is not a bug and it is not a performance setting. React reserves the right to render without committing and to remount a subtree with its state preserved, so any component that misbehaves when it runs twice is already broken under concurrent rendering; Strict Mode just makes it break now rather than in production. Say "development only, and it is finding a real bug" - that answer alone marks you as someone who has read the docs rather than filed an issue.
Follow-up What kind of bug does it actually catch?
Impure renders - mutating a prop, pushing to a module-level array, incrementing a counter in the body - and effects with a missing cleanup, like a subscription or interval that is set up twice and torn down once.
27What is batching?
Grouping several state updates into a single re-render. Two setState calls in the same handler produce one render and one commit, not two.
Without it, a handler that sets four pieces of state would render four times and commit four times, and the user could see intermediate states. Batching is also why state does not change synchronously after you call the setter - the value you read on the next line is still the current render's value, because the new render has not happened yet.
Follow-up What changed in React 18?
Automatic batching everywhere. React 17 batched only inside React event handlers; updates in a setTimeout, a promise callback or a native event listener each rendered separately. React 18 with createRoot batches all of them.
28How do I opt out of batching?
flushSync from react-dom, which forces React to render and commit that update synchronously before continuing.
It exists for the case where you must read the DOM after an update within the same event - measuring an element you just added, or scrolling to a row before the browser paints. It costs you the batch, so two flushSync calls are two renders and two commits. Reach for it rarely and be able to say why.
29Effects run after commit - what is the difference between useEffect and useLayoutEffect?
useLayoutEffect runs synchronously after the DOM is mutated but before the browser paints. useEffect runs after the paint, asynchronously.
So useLayoutEffect is what you use to measure or adjust layout without the user seeing a flicker - a tooltip that must be positioned before it is visible. It also blocks the paint, so putting slow work or a fetch in one is a visible stall. Default to useEffect and move to layout only when you can see a flash.
30In what order do renders and effects run through a tree?
Renders go top down - parent renders, then children. Effects run bottom up - the deepest child's effect fires first, the root's last.
The reason is that a parent's effect may depend on its children's DOM already existing and their effects already having run, and the child's DOM is definitely there by the time the parent's effect fires. Cleanups run in the same bottom-up order on unmount, and React flushes all passive cleanups before any passive setups in a commit that both removes and adds nodes.
31What are React Server Components?
Components that run only on the server, at request or build time, and never ship to the browser. They render to a serialised description of the tree that the client React merges into the page - so their code, and the libraries they import, are not in the bundle at all.
The point is bundle size and data access: a server component can await a database query directly and can import a heavy markdown or date library at zero client cost. The constraint is that they have no state, no effects and no event handlers, because there is no instance in the browser - the moment you need interactivity you mark a boundary with "use client" and that subtree ships normally. They are not SSR: SSR runs your client components on the server once and then hydrates them; server components never hydrate because they never become client code.
32What is hydration?
Attaching React to server-rendered HTML that is already in the page - walking the existing DOM, matching it to the component tree, and wiring up state and event handlers without recreating the nodes.
It is why a server-rendered page can look finished before it is interactive. A hydration mismatch happens when the client's first render disagrees with the markup the server produced - typically because the render read something that differs between the two, like Date.now, Math.random, window, or a value from localStorage. The fix is to render the same thing on both and move the browser-only value into an effect.
33What are the real disadvantages of React?
It is a view library, so every other decision - routing, data fetching, forms, state - is yours to make and to keep making as the ecosystem churns. It ships a runtime you pay for on every page. And the model has sharp edges the tooling cannot hide: stale closures, dependency arrays, and re-render behaviour you have to reason about rather than observe.
Being honest here scores better than defending it. Add the specific ones: JSX means a build step, so there is no runnable page without a toolchain; the escape hatches (refs, effects, imperative DOM) are exactly where teams write their worst code; and the documentation churn is real - class components, then hooks, then server components, each with a body of blog posts that is now wrong. What you get in exchange is a huge hiring pool, a model that scales to large teams, and a rendering target that is no longer only the browser.
Follow-up When would you not reach for React?
A mostly static content site, where a server-rendered page or a lighter tool ships less and does the same job; a page where a hundred kilobytes of runtime is a real cost; and anything where the interaction is one form and one dropdown.
34Is the virtual DOM the reason React is popular?
No. Plenty of frameworks now do the same job without one - compiling the update path ahead of time instead of diffing at runtime. What React has is the component model, the ecosystem and the hiring pool.
This is a good question to answer without defensiveness. The diff was the novel part in 2013; today it is an implementation detail, and React itself has spent a decade making it interruptible rather than making it faster. Being able to say that the virtual DOM is a means rather than the point is exactly the signal being tested.
35React only re-renders what changed - is that statement true?
It is true of the DOM and false of components. By default, when a component re-renders, all of its descendants re-render too, whether or not their props changed.
React does not compare props automatically - it re-runs the subtree and lets the diff decide what to write. The comparison you might expect is opt-in: React.memo for a function component, shouldComponentUpdate or PureComponent for a class. Say the default out loud, because the number of people who believe React skips unchanged children is very high and the interviewer is usually checking for exactly this.
36Why does React re-render children when a parent's state changes, even children that were passed in as props?
It re-renders any child whose element was recreated during the parent's render. A child passed in through props or children was created by the grandparent, so its element object is unchanged and React skips that subtree entirely.
This is the least-known bailout in React and one of the most useful. During reconciliation, if the new element is referentially identical to the previous one, React does not re-render it. So a component that owns state but renders {children} does not re-render those children when its own state changes - they came from above and were not recreated.
37How does React know which component a hook call belongs to?
By position. Hooks are stored as an ordered list on the fiber for the component currently being rendered, and React walks that list in order on every render.
That is the entire reason for the rules of hooks: no calls inside conditions or loops, and only from components or other hooks. Skip a useState on the second render and every subsequent hook reads the previous one's slot - you get someone else's state with no error. It also explains why two instances of the same component do not share state: same list shape, different fiber.
38How would you explain the virtual DOM to a junior engineer in thirty seconds?
You describe what the page should look like. React builds that description as plain objects, compares it with the description it built last time, and changes only the parts of the real page that differ - so you never write the change, only the destination.
Being able to compress it is part of what is being tested - several interviewers ask for the explain-it-simply version specifically. Keep "plain objects", "compare with last time" and "change only the difference", and drop everything else.
Predict the output
The output is hidden. Predict it before you open it.
What a JSX tag evaluates to
const el = <h1 className="title">hi</h1>;
console.log(typeof el, el.type, el.props.className, el.props.children);Show the output
object h1 title hiA JSX tag is an expression that returns a plain object. type is the string "h1" because the tag is lowercase; everything you wrote as an attribute lands in props, and the text between the tags lands in props.children. No DOM node exists yet - this object is the virtual DOM.
JSX and createElement are the same thing
const a = <div id="x">hi</div>;
const b = React.createElement("div", { id: "x" }, "hi");
console.log(a.type === b.type, a.props.id === b.props.id, a === b);Show the output
true true falseSame type, same props - and two different objects, because each call creates a fresh one. That last false is the point: React elements are values you create and throw away on every render, not stable handles onto anything. Identity between renders is decided by type and key, never by object equality.
children is not always an array
const two = <ul><li>a</li><li>b</li></ul>;
const one = <ul><li>a</li></ul>;
console.log(Array.isArray(two.props.children), two.props.children.length);
console.log(Array.isArray(one.props.children), one.props.children.type);Show the output
true 2
false liWith more than one child, props.children is an array. With exactly one, it is that child itself. This is why you never map over props.children directly - use React.Children.map or React.Children.toArray, which normalise both shapes and also skip null and boolean children.
Creating an element does not call the component
function Row() {
console.log("Row ran");
return null;
}
const el = <Row />;
console.log(typeof el.type, el.type === Row, el.type.name);Show the output
function true Row"Row ran" never prints. <Row /> compiles to an object whose type is the function itself - a description of what to render, not the result of rendering it. React calls the function later, during its render phase, and only if that element actually ends up in the committed tree. This is why passing <Slow /> as a prop costs nothing until it is rendered.
key lives on the element, not in props
const row = <li key="a1" id="a1">first</li>;
console.log(row.key, row.props.id, row.type);Show the output
a1 a1 likey is lifted out of the attributes and onto the element object itself, so a component can never read its own key from props - pass it twice, as key and as an ordinary prop, if the component needs the value. ref is treated the same way in the classic transform.
Render order versus effect order
function Child() {
console.log("child render");
useEffect(() => console.log("child effect"), []);
return null;
}
function Parent() {
console.log("parent render");
useEffect(() => console.log("parent effect"), []);
return <Child />;
}
// createRoot(node).render(<Parent />) - no StrictModeShow the output
parent render
child render
child effect
parent effectRendering goes top down: React has to call the parent to find out that Child exists. Effects go bottom up, after the whole commit, so a parent's effect can rely on every child's DOM being in place and every child's effect having already run.
Automatic batching, React 18
function Counter() {
const [a, setA] = useState(0);
const [b, setB] = useState(0);
console.log("render", a, b);
return (
<button onClick={() => setTimeout(() => { setA(1); setB(2); }, 0)}>
go
</button>
);
}
// createRoot(node).render(<Counter />) - then one clickShow the output
render 0 0
render 1 2One render for two updates, even though they happen inside a setTimeout rather than a React event handler. That is React 18's automatic batching with createRoot. Under React 17 the same code printed render 1 0 and then render 1 2 - two renders - because batching applied only inside React's own event handlers.
flushSync opts out
function Counter() {
const [a, setA] = useState(0);
const [b, setB] = useState(0);
console.log("render", a, b);
return (
<button onClick={() => {
flushSync(() => setA(1));
setB(2);
}}>go</button>
);
}
// one clickShow the output
render 0 0
render 1 0
render 1 2flushSync forces that update to render and commit before the next line runs, so you get two renders and two commits instead of one. Use it only when you must read or scroll the DOM between two updates in the same event; it costs you the batch every time.
Strict Mode in development
function App() {
console.log("render");
useEffect(() => {
console.log("subscribe");
return () => console.log("unsubscribe");
}, []);
return null;
}
// createRoot(node).render(<StrictMode><App /></StrictMode>)
// development build, React 18+Show the output
render
render
subscribe
unsubscribe
subscribeThe body runs twice to expose impure renders, and the effect is set up, torn down and set up again to expose a missing cleanup. In production this prints render once and subscribe once. If your effect breaks under this - a doubled subscription, a doubled analytics event - the effect is missing a cleanup, and would break the same way when React remounts a subtree for real.
A component defined inside another component
function Parent() {
const [n, setN] = useState(0);
function Child() {
useEffect(() => {
console.log("child mounted");
return () => console.log("child unmounted");
}, []);
return null;
}
return <div><Child /><button onClick={() => setN(n + 1)}>+</button></div>;
}
// mount, then one clickShow the output
child mounted
child unmounted
child mountedEvery Parent render creates a brand new Child function, so the element's type is a different value than last time. Heuristic one applies: different type, different tree - React unmounts the old subtree and mounts a new one. All of Child's state, DOM and scroll position is destroyed on every keystroke in Parent. Define components at module level, always.
Write it from memory
Complete, runnable code. Practice typing it from memory before your interview.
A virtual DOM in twenty lines
The best way to prove you understand what the virtual DOM is: build the object model and the renderer. Runnable in a plain HTML page, no React.
// The element factory - this is all createElement does.
function h(type, props, ...children) {
return { type, props: { ...props, children: children.flat() } };
}
// The renderer - turn the object tree into real DOM.
function mount(node, container) {
if (node === null || node === false || node === undefined) return;
if (typeof node === "string" || typeof node === "number") {
container.appendChild(document.createTextNode(String(node)));
return;
}
// A function type is a component: call it and mount what it returns.
if (typeof node.type === "function") {
mount(node.type(node.props), container);
return;
}
const el = document.createElement(node.type);
for (const [key, value] of Object.entries(node.props)) {
if (key === "children") continue;
if (key.startsWith("on")) el.addEventListener(key.slice(2).toLowerCase(), value);
else if (key === "className") el.setAttribute("class", value);
else el.setAttribute(key, value);
}
node.props.children.forEach((child) => mount(child, el));
container.appendChild(el);
}
// Use it.
function Row(props) {
return h("li", { className: "row" }, props.label);
}
const tree = h(
"ul",
{ id: "list" },
h(Row, { label: "one" }),
h(Row, { label: "two" })
);
console.log(tree.type, tree.props.children.length); // ul 2
mount(tree, document.body);- h is React.createElement, minus keys, refs and validation. The tree it returns is the virtual DOM: plain objects, no DOM nodes.
- Note that Row is not called until mount reaches it. Creating the element is free.
- What is missing is the interesting half - a diff that takes an old tree and a new tree and produces the minimum set of mutations. Say that out loud: this mounts, it does not reconcile.
A keyed list that survives reordering
The list question, written the way you want it to look under someone's eye: stable keys, a reorder, and state inside each row that proves the keys are doing their job.
import { useState } from "react";
const SEED = [
{ id: "a1", label: "alpha" },
{ id: "b2", label: "beta" },
{ id: "c3", label: "gamma" },
];
function Row({ item }) {
// Local, uncontrolled state - this is what index keys destroy.
const [note, setNote] = useState("");
return (
<li>
<span>{item.label}</span>
<input
value={note}
onChange={(e) => setNote(e.target.value)}
placeholder="note"
/>
</li>
);
}
export default function List() {
const [items, setItems] = useState(SEED);
function addToFront() {
const id = String(items.length + 1) + "z";
setItems((prev) => [{ id, label: "new " + id }, ...prev]);
}
function reverse() {
setItems((prev) => [...prev].reverse());
}
return (
<div>
<button onClick={addToFront}>add to front</button>
<button onClick={reverse}>reverse</button>
<ul>
{items.map((item) => (
<Row key={item.id} item={item} />
))}
</ul>
</div>
);
}- Type a note into a row, then click add-to-front. With key={item.id} the note stays with its row. Change it to key={index} and the note stays with the POSITION - it is now attached to the wrong item.
- setItems takes the updater form because the new value is derived from the old one. Two clicks in one tick would otherwise both read the same stale array.
- reverse copies before reversing: Array.prototype.reverse mutates, and mutating state in place gives React the same array reference, so it bails out and nothing re-renders.