Concept
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 aCounter), 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>, orCounterbecameTimer), 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>
// → full UNMOUNT of the old subtree + MOUNT of a new one,
// even though Counter itself "looks the same" both timesKeys: how React reconciles lists
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.
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.
Index-as-key: why it silently breaks
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.
items.map((item, i) => (<Row key={i} text={item} /> // ❌ index as key))
(nothing yet)
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".
items.map((item) => (<Row key={item.id} text={item.text} /> // ✅ stable, unique id))
(nothing yet)
Initial render. Each Row is keyed by a STABLE id that travels with the item's actual data, not its position in the array.
Type changes tear down the whole subtree, regardless of the children
function Panel({ isHighlighted }) {return (<div className="box"><Counter /></div>);}
(nothing yet)
The user has clicked Counter's internal +1 button five times, count = 5, living in Counter's own state, nested inside a <div>.
Interactive Tree ControlsLive React State
Virtual DOM Component Tree
Rendered DOM OutputLive Document
Render Reason Diagnostics
React.memo skipped render (props shallowly equal)
Parent re-rendered + no React.memo boundary
Try It
Predict what happens before checking the solution.
function Panel({ variant }) {
return variant === "compact"
? <div><Content /></div>
: <section><Content /></section>;
}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.
Implement It Yourself
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).filter(
(key) => newNode.props[key] !== oldNode.props[key]
);
return { op: "update", changedProps, node: newNode };
}
reconcile(
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.
Under the Hood
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.
Common Mistakes
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
{isBig ? <div className="big"><Widget /></div> : <span className="small"><Widget /></span>}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).
Best Practices
- Always use a stable, unique identifier as
keyfor 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
keyis metadata for React, not a prop for your component, pass a separate prop if the component itself needs that value.
Performance Tips
- 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."
