Skip to content

Rendering & Reconciliation

A common misconception: “re-render” means “React touches the DOM.” It doesn’t. React splits the work into two distinct phases:

flowchart LR
  trigger["state change /
parent re-render"] --> render["Render phase
(call components, build new tree)
— pure, no DOM"]
  render --> recon["Reconciliation
(diff new tree vs old)"]
  recon --> commit["Commit phase
(apply minimal DOM changes)
— side effects here"]
The two phases of an update
  1. Render phase — React calls your components to build a new tree of elements. This is pure computation: no DOM is touched, and React can pause or throw it away.
  2. Commit phase — React compares the new tree to the previous one, computes the minimal set of DOM operations, and applies them.

So a re-render that produces the same output as before results in zero DOM changes. Rendering is cheap-ish; the expensive part (touching the DOM) only happens for what actually changed.

Comparing two arbitrary trees is famously expensive (O(n³)). React makes it fast (O(n)) with two heuristics:

  • Different type → throw it away. If an element’s type changed (<div> became <span>, or ComponentA became ComponentB), React unmounts the old subtree entirely and builds the new one fresh — including destroying its state.
  • Same type → keep the node, update props. If the type is the same, React keeps the existing DOM node and its state, and just updates the changed props.
// Same type across renders → the <input> keeps its DOM node and its focus/value.
{isEditing ? <input value={text} /> : <input value={text} disabled />}
// Different type → React destroys the first and mounts the second (state lost).
{isEditing ? <input value={text} /> : <p>{text}</p>}

For a list of siblings of the same type, React needs to know which item is which across renders. By default it matches by position (index) — which breaks the moment items reorder, insert, or delete. A key gives each item a stable identity so React can track it.

// ✅ Stable identity — React follows each todo even when the list reorders.
{todos.map((todo) => <TodoItem key={todo.id} todo={todo} />)}
// ⚠️ Index as key — reorder/insert/delete and React mismatches items,
// corrupting per-item state (a checked box jumps to the wrong row).
{todos.map((todo, i) => <TodoItem key={i} todo={todo} />)}
flowchart TB
  before["Before: [A, B, C]
keys a,b,c"] -->|prepend Z| after["After: [Z, A, B, C]
keys z,a,b,c"]
  after --> good["With stable keys:
A,B,C reused, Z mounted"]
  before2["Before: [A, B, C]
key = index 0,1,2"] -->|prepend Z| after2["After: index 0,1,2,3"]
  after2 --> bad["With index keys:
every item mismatched,
state shifts to wrong rows"]
Keys let reconciliation track items across a reorder

The rule: a key must be stable, unique among siblings, and tied to the data’s identity (usually a database id) — never the array index for a list that can change, and never Math.random() (which changes every render and forces a full remount).

What happens during the "render phase"?
During reconciliation, what does React do when an element's type is the SAME as before?
Why is using the array index as a key risky for a list that can reorder?
If a re-render produces the exact same output as before, how many DOM changes occur?