ReactReconciliation & the Diffing Algorithm

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. divspan, or CounterTimer), it tears down the entire subtree and builds a new one from scratch. All component state and DOM nodes in that subtree are destroyed.

JSX
// 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>
Warning
Changing the wrapper element type (even from `div` to `section`) forces every child to remount. Counter state, input focus, animation state — all gone. Keep root element types stable.

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.

JSX
// 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.

JSX
// 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-rendered

Scenario

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.

JSX
// 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.

JSX
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.
Tip
The "key to reset" pattern is simpler and more reliable than writing a `useEffect` that manually resets every piece of state when a prop changes.
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/section swaps that destroy subtrees

  • Always use stable, unique keys for dynamic lists — never use array index for lists that reorder or prepend

  • Use key intentionally to reset component state instead of fighting with useEffect

  • Wrap expensive but non-urgent updates in useTransition to keep input responsive

  • Understand that React.memo and useMemo don't skip reconciliation entirely — they skip calling your render function, but the fiber node still exists

Note
React's diffing algorithm is an approximation — it trades exactness for speed. When you give React better hints (stable keys, stable element types), you help it make better decisions and your app gets faster for free.