React's Reconciliation Algorithm Explained
When you call setState or update a prop, React doesn't just throw the whole UI away and rebuild it from scratch. That would be catastrophically slow. Instead it runs a comparison process — called reconciliation — to find the minimum number of DOM operations needed to move from the current UI to the next one.
Understanding how this algorithm works isn't just academic. It directly affects performance, explains why certain bugs happen, and tells you exactly why key props matter.
The Naive Problem: Tree Diffing Is Expensive
A UI can be modeled as a tree of elements. When state changes, React has a new tree it wants to render. The computer science problem of finding the difference between two arbitrary trees has a complexity of O(n³) — meaning for 1,000 elements, you'd need roughly 1 billion operations.
That's unusable in a browser. React's reconciler solves this by making two practical assumptions that bring the complexity down to O(n):
- Elements of different types produce completely different trees.
- A
keyprop signals which child elements are stable across renders.
These two heuristics let React skip enormous swaths of comparison work.
Assumption 1: Type Changes Mean Full Replacement
When React compares two nodes and finds that the element type has changed, it tears down the entire subtree rooted at that node and builds a fresh one. It doesn't try to reconcile children.
// Before
<div>
<Counter />
</div>
// After
<section>
<Counter />
</section>
Here, div changed to section. React destroys the div and everything inside it — including Counter — and builds a new section with a fresh Counter instance. The old Counter's state is lost.
This is why replacing a wrapper element type is a bigger deal than it looks. It's not just a cosmetic change; it resets all descendant state.
If the types match, React keeps the existing DOM node and only updates the changed attributes:
// Before
<div className="box" id="main" />
// After
<div className="card" id="main" />
React recognises both are divs and only updates the className attribute — no DOM node is created or destroyed.
Assumption 2: Keys Identify Stable Children
Lists are where reconciliation gets interesting and where the most common bugs live.
<ul>
<li>Alice</li>
<li>Bob</li>
</ul>
If you add a new item at the end, React compares the first two children, finds they match, and appends the third. Easy.
But if you insert at the beginning:
<ul>
<li>Zara</li> {/* new */}
<li>Alice</li>
<li>Bob</li>
</ul>
Without keys, React compares by position. It sees the first child changed from Alice to Zara, the second changed from Bob to Alice, and there's a new third item Bob. It mutates every single node — three operations instead of one.
With key props, React can match nodes by identity across renders rather than by position:
<ul>
{users.map((user) => (
<li key={user.id}>{user.name}</li>
))}
</ul>
Now React knows that key="alice" is Alice regardless of where she appears in the list. It moves the existing DOM node instead of re-creating it. One operation.
Why Index-as-Key Breaks Things
A common shortcut is using the array index as a key:
// ❌ Fragile
{items.map((item, index) => (
<li key={index}>{item.name}</li>
))}
The problem: when items are reordered or inserted, the indexes shift. key={0} still refers to "the first item," not the same item as before. React treats each node as stable, so it doesn't unmount and remount components — it just passes new props into the same component instance. If that component has its own internal state (e.g. a controlled input, an animation, a timer), that state now belongs to the wrong item.
// ✅ Stable
{items.map((item) => (
<li key={item.id}>{item.name}</li>
))}
Use a stable, unique identifier — a database ID, a UUID, anything that doesn't change when the list order changes.
Keys Can Force a Full Remount
The flip side: you can use key intentionally to force React to treat a component as a new instance.
<UserForm key={selectedUserId} userId={selectedUserId} />
When selectedUserId changes, the key changes, React unmounts the old UserForm entirely and mounts a fresh one. All internal state resets automatically — saving you from manual useEffect cleanup logic to reset form fields.
This is intentional use of the reconciliation rule: same type + different key = full replacement.
What Reconciliation Doesn't Do
It's worth being explicit about the limits:
- Reconciliation compares the virtual DOM (the lightweight JS objects React builds), not the real DOM directly. Only after it decides what changed does it apply those changes to the real DOM in a separate "commit" phase.
- It doesn't look inside
childrenwhen a type mismatch is found — the entire subtree is replaced. - It doesn't compare across siblings of different keys. Each key-identified node is matched independently.
Practical Takeaways
Think about type stability. If you conditionally render <Input type="text"> vs <Input type="email">, that's fine — same type. But if you switch between <TextInput> and <SelectInput> (different component functions), React will remount.
Use stable keys everywhere you map. item.id is almost always the right answer.
Use intentional key resets sparingly. Forcing a remount is a legitimate escape hatch but it's not free — the old component unmounts, runs cleanup effects, and the new one mounts from scratch.
Don't place components inside component definitions. Defining a component function inside a render function means it's a new function reference every render, so React treats it as a different type and remounts it every time.
// ❌ New function on every render → always remounts
function Parent() {
function Child() { return <p>Hi</p> }
return <Child />
}
// ✅ Stable reference → reconciles normally
function Child() { return <p>Hi</p> }
function Parent() { return <Child /> }
Conclusion
React's reconciliation algorithm is built on two simple rules — type changes destroy subtrees, and keys identify stable children — that make updating real-world UIs fast without a developer having to think about DOM operations at all. Understanding these rules turns confusing bugs (wrong state in a list item, unexpected resets, stale components) into predictable behaviour you can reason about and control.
