Skip to content

Reconciliation and Diffing Algorithm

advanced13 min read

The Algorithm That Makes React Possible

When you call setState, React doesn't throw away the DOM and rebuild it. It compares the new element tree against the old one and figures out the minimum set of DOM operations needed. This comparison is the reconciliation algorithm.

A general tree diff is O(n^3) — for 1,000 nodes, that's a billion comparisons. React does it in O(n) with two heuristics that turn out to be correct 99.9% of the time.

// Before: React sees this tree
<div>
  <Header />
  <UserList users={oldUsers} />
</div>

// After setState: React sees this tree
<div>
  <Header />
  <UserList users={newUsers} />
</div>

// React's diff:
// 1. <div> === <div> → same type → keep DOM node, check children
// 2. <Header> === <Header> → same type → compare props (none changed → skip)
// 3. <UserList> === <UserList> → same type → compare props (users changed → re-render)

The Mental Model

Mental Model

Think of reconciliation as a building inspector comparing blueprints. The inspector doesn't measure every wall, wire, and pipe from scratch. They overlay the old blueprint on the new one and look for differences floor by floor:

  • Same room type in the same position? Check if the room's specs changed. If the living room is still a living room, just check for new paint or furniture.
  • Different room type? Demolish the old room and build the new one from scratch. Don't try to convert a bathroom into a kitchen.
  • Rooms in a list (apartment complex)? Match rooms by their apartment numbers (keys), not by their position in the hallway.

This is O(n) because the inspector only looks at each room once, comparing it to the corresponding room in the new blueprint.

Heuristic 1: Same Type = Update, Different Type = Destroy

React compares elements at the same position in the tree. The first check is always the element type:

// Same type: React updates the existing DOM node
// Before:
<div className="old" />
// After:
<div className="new" />
// React: Keep the DOM node. Change className from "old" to "new".

// Different type: React destroys the old tree and builds new
// Before:
<div><Counter /></div>
// After:
<span><Counter /></span>
// React: Destroy the entire <div> subtree (including Counter's state).
//        Build a fresh <span> subtree. Counter starts from initial state.

This has a critical implication: changing the wrapper element destroys all child state.

function App({ isAdmin }) {
  // Every time isAdmin changes, Counter loses its state
  if (isAdmin) {
    return <section><Counter /></section>;
  }
  return <div><Counter /></div>;
}

React sees <section> change to <div>. Different types. It unmounts the entire <section> subtree and mounts a fresh <div> subtree. Counter's state resets to 0.

Execution Trace
Compare:
`section` vs `div`
Different type → destroy old subtree entirely
Unmount:
section → Counter
Run Counter's cleanup effects. Remove DOM nodes. Discard all state
Mount:
div → Counter
Create new DOM nodes. Run Counter from scratch. State = initial value
Result:
Counter resets to 0
The user's input is lost because React sees this as a new component

Component Type Identity

For components, React compares the function/class reference itself:

// These are different components even though they render identical output
const ButtonA = () => <button>Click</button>;
const ButtonB = () => <button>Click</button>;

function App({ useNew }) {
  // Toggling between ButtonA and ButtonB destroys and remounts
  return useNew ? <ButtonA /> : <ButtonB />;
}

React checks ButtonA === ButtonB — they're different function references, so the old fiber is destroyed and a new one is created.

Common Trap

Defining components inside render functions creates a new function reference every render:

function App() {
  // BUG: This creates a new component type on every render
  const Input = () => <input />;
  return <Input />; // Unmounts and remounts every render — loses focus, loses input
}

The Input function is created fresh each render. React sees a "new" component type each time, destroying the old <input> DOM node and creating a new one. The user's cursor position and typed text are lost. Always define components outside the render function.

Heuristic 2: Keys Identify Elements in Lists

When React encounters a list of children, it can't rely on position alone. Consider reordering:

// Before:
<ul>
  <li>Alice</li>
  <li>Bob</li>
  <li>Charlie</li>
</ul>

// After (Alice removed):
<ul>
  <li>Bob</li>
  <li>Charlie</li>
</ul>

Without keys, React compares by position:

  • Position 0: AliceBob. Same type <li>. React updates text from "Alice" to "Bob".
  • Position 1: BobCharlie. Same type <li>. React updates text from "Bob" to "Charlie".
  • Position 2: Charlie → nothing. React removes the third <li>.

Result: three DOM operations (two text updates + one removal). But the optimal answer is one operation: remove Alice's <li>.

With keys:

<ul>
  <li key="alice">Alice</li>
  <li key="bob">Bob</li>
  <li key="charlie">Charlie</li>
</ul>
// After:
<ul>
  <li key="bob">Bob</li>
  <li key="charlie">Charlie</li>
</ul>

React matches by key: Bob's <li> stays, Charlie's <li> stays, Alice's <li> is removed. One DOM operation.

The List Reconciliation Algorithm

The actual algorithm is more nuanced than simple key matching. If you want to know exactly what happens when React processes your list, here it is:

For the new list of children:
1. Build a map: oldKey → oldFiber for all existing children
2. For each new child:
   a. Look up its key in the map
   b. If found: reuse the old fiber, update props, remove from map
   c. If not found: create a new fiber (Placement flag)
   d. Track whether the reused fiber needs to move (its old index < lastPlacedIndex)
3. Any remaining fibers in the map are deletions (Deletion flag)
Execution Trace
Old list:
[A:0, B:1, C:2, D:3]
Keys A-D at indices 0-3
New list:
[C, A, D, B]
Same items, different order
Process C:
Found C at old index 2
lastPlacedIndex = 2. No move needed (first item)
Process A:
Found A at old index 0
0 < 2 (lastPlacedIndex) → A needs to MOVE
Process D:
Found D at old index 3
3 >= 2 → no move. lastPlacedIndex = 3
Process B:
Found B at old index 1
1 < 3 (lastPlacedIndex) → B needs to MOVE
Result:
Move A and B
2 DOM moves instead of rebuilding all 4 items

The lastPlacedIndex optimization avoids unnecessary moves: items that are already in the right relative order stay put. Only items that moved backward (relative to the previous iteration) get repositioned.

Why O(n) instead of optimal O(n) LIS?

The optimal reordering algorithm would find the Longest Increasing Subsequence (LIS) of old indices and only move items not in the LIS. React's algorithm is simpler: it scans left to right and moves anything that went backward.

For the example [C, A, D, B] from [A, B, C, D]:

  • LIS approach: keep C and D in place, move A and B. 2 moves (optimal).
  • React's approach: same result here, but in other cases it can produce more moves than LIS.

React chose simplicity over optimality. The algorithm is easier to implement, easier to reason about, and the difference rarely matters in practice — most list updates are insertions, deletions, or small reorderings, not worst-case shuffles.

DOM Operation Flags

During reconciliation, React doesn't touch the DOM. Instead, it tags fibers with effect flags:

// Effect flags set during reconciliation
const Placement = 0b0000000000000010;    // New node — insert into DOM
const Update =    0b0000000000000100;    // Props changed — update DOM attributes
const Deletion =  0b0000000000001000;    // Removed — delete from DOM
const ChildDeletion = 0b0000000000010000; // A child was deleted

These flags accumulate during the render phase. The commit phase reads them and performs the actual DOM mutations in a single batch.

Production Scenario: The Expensive Remount

A team has a tabbed interface where each tab renders a complex form:

function TabbedForm({ activeTab }) {
  // BUG: Different wrapper elements destroy child state
  switch (activeTab) {
    case 'basic':
      return <div className="tab-basic"><BasicForm /></div>;
    case 'advanced':
      return <section className="tab-advanced"><AdvancedForm /></section>;
    case 'admin':
      return <article className="tab-admin"><AdminForm /></article>;
  }
}

Every tab switch destroys the form state because the wrapper element type changes (divsectionarticle). Users lose their in-progress form data.

The fix uses the same element type with different props:

function TabbedForm({ activeTab }) {
  return (
    <div className={`tab-${activeTab}`}>
      {activeTab === 'basic' && <BasicForm />}
      {activeTab === 'advanced' && <AdvancedForm />}
      {activeTab === 'admin' && <AdminForm />}
    </div>
  );
}

Now the wrapper is always <div>. React updates the className prop without destroying children. But each form still unmounts when its tab isn't active — sometimes that's desired (reset on switch), sometimes not.

For preserving state across tabs, use CSS visibility or display: none, or lift state out of the form components.

Common Mistakes

Common Mistakes
  • Wrong: Using array index as key for dynamic lists Right: Use stable, unique identifiers (database IDs, UUIDs) as keys

  • Wrong: Defining components inside render to 'keep things close' Right: Define components at module scope or in separate files

  • Wrong: Changing wrapper element types conditionally (div → section → article) Right: Use the same element type and change only props, or use keys to intentionally reset

  • Wrong: Assuming React does deep equality comparison of the element tree Right: React only compares type and key at each position. Props are passed to the component for it to decide

Challenge

Predict the DOM operations

Show Answer

DOM operations:

1. **Destroy `<h1>` and create `<h2>`**`h1``h2` is a type change. React removes the `<h1>` node and creates a new `<h2>` with text "New Title".
2. **Update `<p>` text content** — Same type `<p>`, same key "intro". React updates the text from "Welcome!" to "Hello!". No DOM removal.
  1. Move <li key="a"> — Charlie (key "c") is at old index 2, now at new index 0. lastPlacedIndex = 2. Alice (key "a") was at old index 0, which is less than 2 → Alice needs to move. Also update text to "Alice (updated)".

  2. Delete <li key="b"> — Bob's key "b" is not in the new list. React removes this DOM node.

  3. Insert <li key="d"> — Diana's key "d" doesn't exist in the old list. React creates and inserts a new <li> node.

Total: 1 destroy + 1 create + 1 text update + 1 move + 1 text update + 1 delete + 1 insert = 5 DOM mutations (compared to the naive approach of destroying and rebuilding all 6 elements).

Quiz

Quiz
What happens to Counter's state when isLoggedIn changes?
Quiz
Given the old list [A, B, C, D] and new list [D, A, B, C] (all with stable keys), how many DOM move operations does React perform?

Key Rules

Key Rules
  1. 1React's diff is O(n) based on two heuristics: same type = update, different type = destroy and rebuild.
  2. 2Element type comparison is by reference (=== for functions/classes, string comparison for DOM elements).
  3. 3Changing a wrapper element type destroys all child state — every component in the subtree remounts.
  4. 4Keys let React match list items by identity instead of position. Always use stable, unique keys for dynamic lists.
  5. 5Never define components inside render functions — each render creates a new type reference, causing unmount/remount.
  6. 6Reconciliation only sets effect flags. Actual DOM mutations happen in the commit phase.