The Virtual DOM isn't a performance trick — it's a comparison mechanism. React diffs the new element tree against the last one it committed, and the ONE piece of information that diff runs on is each element's type plus its key. Nearly every list bug and every reconciliation interview question traces back to that single fact.
reconciliationvirtual-domkeysdiffingadvanced
Knowledge Check
4 questions · pass at 70%
0/4
easymcq
1. What are the TWO pieces of information React's reconciliation uses to decide update-vs-replace at a given tree position?
Interview Questions
2 questions
Cheatsheet
Download-ready reference
Cheatsheet
Reconciliation = the diffing algorithm between the new element tree
and the last COMMITTED tree. Two rules, per tree position:
SAME type at this position → efficient UPDATE (props diffed,
instance/DOM node REUSED)
DIFFERENT type at this position → full UNMOUNT of old subtree +
MOUNT of new one — no children
diffing attempted at all
Lists: React matches items by KEY, not position.
Same key in old + new render → same logical item (update/move).
New key → mount. Missing key → unmount.
Index-as-key = position IS identity. Breaks the moment items are
inserted/removed/reordered anywhere except pure append-at-end —
DOM state (inputs, focus) can attach to the WRONG logical item.
key is NOT a prop — React strips it before constructing props.
Need an identifier inside the component? Pass a separate prop.
Changing an element's TYPE at a given position (even a wrapping
<div> → <span>) destroys everything below it, including
components whose own code never changed.
The beginner framing: React keeps a lightweight in-memory copy of the UI — the "Virtual DOM" — and compares the new version against the old one to figure out the minimal set of real DOM changes needed, instead of rebuilding the whole page from scratch on every update.
The precise mental model: reconciliation is the diffing algorithm React runs between the element tree a render just produced and the tree it committed last time. Rebuilding real DOM nodes is expensive; comparing two in-memory object trees is cheap — so React does the comparison first, and only touches the real DOM for what actually changed. The entire algorithm rests on two rules, applied position by position down the tree:
If the element type at a given position is the same (both a <div>, both a Counter), React keeps the existing DOM node/component instance and just updates its props — an efficient, in-place update.
If the element type at a given position differs (a <div> became a <span>, or Counter became Timer), React doesn't try to reconcile them at all — it tears down the entire old subtree and mounts a completely new one, no matter how similar the two might look.
// Render 1:<div className="box"><Counter /></div>// Render 2 — SAME type at this position (div, then Counter):<div className="highlighted"><Counter /></div>// → efficient UPDATE: className changes on the existing DOM node,// Counter's existing instance (and its state) is preserved// Render 2 (alternative) — DIFFERENT type at this position:<span className="box"><Counter /></span
For a single element, "same position in the tree" is enough to decide same-vs-different. For a list of elements, position alone isn't reliable — items get added, removed, and reordered — so React needs an explicit, stable identity for each item: the key prop.
{items.map((item) => ( <Row key={item.id} text={item.text} /> // stable id, NOT the array index))}
React matches list items by key, not by position: an item whose key appears in both the old and new render is treated as the same item (updated in place, possibly moved), regardless of where it sits in the array. An item whose key is new gets mounted; an item whose key disappeared gets unmounted.
Changing key destroys and recreates a component — its state is lost
live — not a recording
Parentrendered 1×
key = 0 · label = "A"
Arendered 1×
internal clicks: 0
Click "+1 click" a few times to build up StatefulBox's own internal count. Then click "Update label prop (same key)" — the label changes but the click count survives, since it's the same component instance, just re-rendered with a new prop. Now click "Change key (forces remount)" — StatefulBox is destroyed and a brand-new instance is mounted from scratch, and the click count resets to 0, even though the label may be identical to before.
Using the array index as key is really saying "position IS identity" — which is exactly the assumption that breaks the moment items are inserted, removed, or reordered anywhere except the very end of the list.
Index-as-key + prepend = every row mutates in place
step 1 / 2
items.map((item, i) => (
<Row key={i} text={item} /> // ❌ index as key
))
Before
(nothing yet)
After
key: 0untouched
Apples
input: "yum"
key: 1untouched
Bananas
key: 2untouched
Cherries
Initial render. Each Row is keyed by its INDEX. Row 0 ("Apples") has an uncontrolled input where the user typed "yum" — that input's value lives in the real DOM node, attached to key="0".
If Panel re-renders with variant toggling between "compact" and anything else, does Content keep its internal state across that toggle?
Solution
No — Content loses all its internal state every time variant toggles. Even though Content itself is completely unchanged, its parent element at that tree position switches between <div> and <section> — a type change — so React tears down the entire subtree (the wrapper element AND Content's instance inside it) and mounts a fresh one each time. The fix, if Content's state should survive the toggle, is to keep the wrapper element type constant and vary only its props/class, or to restructure so Content isn't nested inside the element that's changing type.
Build a simplified reconciler that implements the two core rules — same-type-updates, different-type-tears-down — directly:
function reconcile(oldNode, newNode) { if (oldNode === null) { return { op: "mount", node: newNode }; } if (oldNode.type !== newNode.type) { // type differs — no attempt to diff children AT ALL, full replace return { op: "unmount+mount", old: oldNode, node: newNode }; } // same type — diff props, keep the same underlying instance const changedProps = Object.keys(newNode.props).
This is the exact shape of React's real algorithm at the single-element level — the moment oldNode.type !== newNode.type, there's no attempt made to salvage anything from the old subtree, which is precisely why a type change is so much more expensive (and so much more state-destructive) than a prop change.
Comparing two trees by type-and-position rather than doing a full, general tree-diff (which is computationally expensive — years of computer science research puts general tree diffing at roughly O(n³)) is a deliberate, pragmatic trade React makes: assume that elements of a different type at the same position are rarely meaningfully related, and get an O(n) algorithm in exchange. This mirrors the same identity-vs-equality distinction from Data Structures and object reference comparison in JavaScript generally — React's type !== type check is just a === reference/value comparison, the cheapest operation available, applied as the very first gate before any deeper work happens.
1. Using the array index as key for a list that can reorder, insert, or remove#
Covered above in depth — index-as-key silently attaches state and DOM identity to a position, not to the actual data, which corrupts as soon as the list order changes for any reason other than pure appending at the end.
2. Wrapping content in a conditionally-different element type just for styling#
This destroys and recreates Widget every time isBig toggles, even though the intent was purely cosmetic. Keep the wrapper type constant and vary only its props/class when the children's state should survive.
3. Assuming key is a regular prop accessible inside the component#
function Row({ key, text }) { // ❌ key is NEVER passed as a prop console.log(key); // always undefined}
key is consumed entirely by React's reconciliation process before the component ever runs — it's not part of props at all. If a component needs its own identifier for logic inside itself, pass it under a different prop name (id, not key).
Always use a stable, unique identifier as key for list items — a database id, a UUID, anything intrinsic to the data — never the array index unless the list is truly static (never reordered, filtered, or prepended to).
Keep a subtree's wrapper element type stable across renders if the content inside needs to preserve its state — vary props and classes, not the tag or component itself, purely for styling changes.
Remember key is metadata for React, not a prop for your component — pass a separate prop if the component itself needs that value.
Reconciliation's type-and-key-based matching is what makes updating a long list cheap: with correct keys, inserting one item at the front of a 10,000-item list is a single mount operation, not 10,000 updates — exactly what list-with-keys demonstrates above.
A type change (Common Mistake #2) is far more expensive than a prop change specifically because it skips diffing entirely and does a full unmount/remount of the whole subtree — profile any surprisingly slow update for a hidden type change at some tree position, not just "too many re-renders."
>
// → full UNMOUNT of the old subtree + MOUNT of a new one,
// even though Counter itself "looks the same" both times