AcademyReact
Reconciliation and Why Keys Matter
The heuristic React uses to decide what to reuse and what to rebuild, and why the wrong key silently attaches state to the wrong row.
4 min readUpdated 3 exercises
The heuristic, not the algorithm
A generic tree-diff is expensive — comparing two arbitrary trees of N nodes is O(n^3) in the general case. React does not attempt it. Instead it applies one heuristic, applied top-down, sibling by sibling:
- Same element type at the same position → keep the underlying instance, patch its props, recurse into its children.
- Different element type at the same position → tear the old instance down completely (including its state) and mount a fresh one in its place.
// Render 1
<UserCard user={a} />
// Render 2
<UserCard user={b} />
// Same type, same position → the same UserCard instance is reused,
// only its props change. Any internal state (open menus, scroll position
// inside the card) survives, now describing user b.// Render 1
<UserCard user={a} />
// Render 2
<AdminCard user={a} />
// Different type at the same position → UserCard is unmounted, its
// state is gone, and a brand new AdminCard mounts from scratch.This is what "component instance" means in React: not the element (a new one is created every render) but the thing React keeps alive across renders because the heuristic said to reuse it.
Where keys come in
The heuristic above works fine for a fixed, ordered set of children. It breaks down for a list, because "position" stops meaning anything useful the moment the list can be reordered, filtered, or have an item removed from the middle.
key is how you opt out of "position decides identity" and tell React what actually decides identity: not where an item sits, but which item it is.
{todos.map((todo) => (
<TodoRow key={todo.id} todo={todo} />
))}With a stable key, React matches old children to new children by key, not by index. Delete the second todo, and React now sees "the row keyed 2 is gone, the rows keyed 1 and 3 are unchanged" — it unmounts exactly one row and leaves the other two, and their DOM nodes and internal state, completely alone.
The array-index bug
Using the array index as the key looks harmless and is the single most common React interview trap, because it reintroduces the exact problem key exists to solve: index is still position.
// Each row has its own local "editing" state
function TodoRow({ todo }) {
const [editing, setEditing] = useState(false);
return editing ? <TodoEditor todo={todo} /> : <TodoLabel todo={todo} onClick={() => setEditing(true)} />;
}
// BAD: key is the index
{todos.map((todo, i) => <TodoRow key={i} todo={todo} />)}Put row 0 into edit mode, then delete row 0 from the underlying array. The remaining first item shifts up to index 0. React compares "the thing keyed 0" before and after — same key, same position — and reuses the same TodoRow instance, patching its todo prop to the new first item. But editing is local state on that instance, and it never got reset. The wrong todo — the one that shifted into position 0 — now renders open in edit mode.
// GOOD: key is the thing's own identity, independent of where it sits
{todos.map((todo) => <TodoRow key={todo.id} todo={todo} />)}Now deleting the edited row deletes the instance keyed to it, editing state and all, and nothing shifts into a state it never had.
What a key actually needs to be
| Requirement | Why |
|---|---|
| Stable across renders | Regenerating it every render (key={Math.random()}) defeats the purpose — every item looks "new," so nothing is ever reused |
| Unique among siblings | React only compares keys within one list; the same key in two different lists never collides |
| Not necessarily globally unique | A todo's id and a comment's id can both be 1 — they live in different lists |
| Not the array index, unless the list is truly static | Static and never reordered/filtered is the only case index is safe |
The index rule of thumb: if the list can change shape — reorder, insert, delete, filter — the index is not a safe key, because it stops describing the item and starts describing a slot.
Now practice it
2 challenges · 1 quizReading this page is the cheap half. These are the exercises that make you use it.
- List Reconciliation by KeyProImplements the actual by-key matching between an old and a new list — the mechanism this page only describes in prose.ChallengeSolve · MediumChallengeSolve · Medium
- Diff Component TreesProExtends the same-type-same-position heuristic to nested trees, where a mismatch has to decide how much of a subtree to discard.ChallengeSolve · MediumChallengeSolve · Medium
- Fundamentals · Component PatternsComponent-pattern questions that lean on knowing what survives a re-render and what gets torn down.QuizTake · MediumQuizTake · Medium
Read next
JSX and the Virtual DOM
JSX is sugar for function calls that build plain objects, not templating — and that object tree is the whole reason React can be fast.
3 min
React.memo and Unnecessary Re-renders
A component re-renders whenever its parent does, props unchanged or not — usually fine, and React.memo is the opt-out for when it measurably is not.
4 min