Reconciliation & the Diffing Algorithm
Every time state or props change, React needs to figure out what has actually changed in the UI and apply the minimum number of DOM mutations. This process is called reconciliation. Understanding it transforms you from someone who writes React into someone who writes fast React.
The Virtual DOM
React maintains a lightweight in-memory representation of the real DOM called the Virtual DOM. When your component re-renders, React builds a new Virtual DOM tree and compares it with the previous one. Only the differences get applied to the real DOM — a technique called diffing.
A naive comparison of two trees would be O(n³) — a 1,000-element tree would require a billion comparisons. React's diffing algorithm uses two heuristic assumptions to bring this down to O(n) linear time.
Assumption 1 — Different Element Types
When React finds that the element type at a given position in the tree has changed (e.g. div → span, or Counter → Timer), it tears down the entire subtree and builds a new one from scratch. All component state and DOM nodes in that subtree are destroyed.
// Before re-render
<div>
<Counter />
</div>
// After re-render — element type changed from div to section
<section>
<Counter /> {/* Counter is UNMOUNTED and remounted — state is LOST */}
</section>When the element type is the same, React keeps the DOM node and only updates the changed attributes. For component elements, the instance is kept alive and its props are updated — state is preserved.
// Before <input type="text" value="hello" className="field" /> // After — same element type, React just patches the attributes <input type="text" value="world" className="field active" /> // Result: React updates value and adds the "active" class. // The input element itself is reused, focus is not lost.
Assumption 2 — The key Prop
When reconciling lists of elements, React needs to match items between renders. Without hints, it uses position. With the key prop, React can track items across reorders, insertions, and deletions.
// Without keys — React matches by index
// If you prepend an item, every existing item updates unnecessarily
const items = ['Alice', 'Bob', 'Carol']
// Before: [Alice@0, Bob@1, Carol@2]
// After prepend: [Dave@0, Alice@1, Bob@2, Carol@3]
// React sees: position 0 changed, 1 changed, 2 changed, 3 is new
// → All three original items re-render unnecessarily
// With stable keys — React matches by identity
items.map(name => <li key={name}>{name}</li>)
// React sees: Dave is new, Alice/Bob/Carol kept their identity
// → Only Dave is inserted, others are not re-renderedScenario | No key (index) | Stable key |
|---|---|---|
Prepend item | All items re-render | Only new item mounts |
Delete middle item | All items after shift | Only deleted item unmounts |
Reorder items | All items update | DOM nodes move, no remount |
Input focus | Focus jumps to wrong input | Focus stays on correct input |
Wrong Keys Force Unnecessary Remounts
Using array index as a key is the most common key mistake. It looks harmless until you prepend or sort items — then every component in the list remounts because its key (the index) changed.
// BAD — using index as key
function TodoList({ todos }) {
return (
<ul>
{todos.map((todo, index) => (
<TodoItem key={index} todo={todo} /> // index key is wrong
))}
</ul>
)
}
// If todos goes from [A, B, C] → [NEW, A, B, C]:
// key=0 now maps to NEW (was A) → remount
// key=1 now maps to A (was B) → remount
// key=2 now maps to B (was C) → remount
// key=3 now maps to C (new) → mount
// Every component remounted — state lost, animations reset
// GOOD — use a stable, unique identifier
function TodoList({ todos }) {
return (
<ul>
{todos.map(todo => (
<TodoItem key={todo.id} todo={todo} /> // stable identity
))}
</ul>
)
}Using key to Force a Reset
The same mechanism works in your favour when you want to reset a component. Changing the key of a component forces React to unmount it and mount a fresh one — useful when you need to fully reset form state when a user selects a different record.
function UserEditor({ userId }) {
// When userId changes, key changes → form unmounts and remounts
// All internal useState values reset to their initial values
return <UserForm key={userId} userId={userId} />
}
// Without key={userId}, UserForm keeps its old state and you'd need
// a useEffect to manually reset it when userId changes — fragile.React Fiber: Async Reconciliation
React 16 rewrote the reconciliation engine as Fiber — a new internal architecture that represents every unit of work as a linked list of fiber nodes rather than a recursive call stack.
Fiber makes reconciliation interruptible. React can pause work in progress, hand control back to the browser to handle user input, and then resume. This is the foundation for concurrent features like useTransition, useDeferredValue, and Suspense.
Priority Lanes
Fiber assigns work to lanes — priority buckets. Higher-priority work (a button click, keyboard input) can interrupt lower-priority work (a background data re-render). React 18's concurrent mode uses this to keep the UI responsive even while processing large updates.
Lane / Priority | Example | Behaviour |
|---|---|---|
Sync (highest) | Controlled input value | Flush immediately, blocking |
Input Continuous | Hover, scroll | Flush before paint |
Default | Normal state update | Batched, async flush |
Transition | useTransition wrapped update | Interruptible, deferrable |
Idle (lowest) | Prefetch, speculation | Only when browser is idle |
The Commit Phase
Reconciliation (diffing) is separate from the commit phase. After React has calculated the minimal diff, it commits all DOM mutations synchronously in a single pass so the browser never shows a partial UI. Side effects (useLayoutEffect, then useEffect) fire after the commit.
Render phase — call your component function, build new Virtual DOM tree, diff with previous
Reconcile phase — calculate minimal set of DOM mutations (can be interrupted in concurrent mode)
Commit phase — apply DOM mutations synchronously, then fire layout effects, then paint, then fire passive effects
Why This Matters for Your Code
Keep root element types stable — avoid conditional
div/sectionswaps that destroy subtreesAlways use stable, unique keys for dynamic lists — never use array index for lists that reorder or prepend
Use
keyintentionally to reset component state instead of fighting withuseEffectWrap expensive but non-urgent updates in
useTransitionto keep input responsiveUnderstand that React.memo and useMemo don't skip reconciliation entirely — they skip calling your render function, but the fiber node still exists