Handbooks  /  React
Handbook~16 min readFrontendworked math + runnable code
The React Handbook

Describe it.
React diffs it.

React's promise is that you write your UI as if you rebuild the whole thing on every change — no manual DOM surgery, just "here's what it should look like now." The trick that makes that fast is reconciliation: React builds a lightweight virtual DOM, diffs it against the previous one, and applies only the minimal real changes. And the humble key is what tells that diff which list items are "the same" across renders. Get keys right and React reuses elements cheaply; get them wrong (hello, array index) and you get baffling bugs. This handbook is that diff, made runnable.

01

Declarative UI

Before React, updating a UI meant imperative DOM surgery: find this node, change its text, add that child, remove the other — and keep it all consistent with your data by hand, which is where bugs breed. React's core idea is declarative: your components are functions of state that return a description of what the UI should look like right now. You never touch the DOM; you just say "given this state, here's the tree."

That description is the virtual DOM — plain JavaScript objects, cheap to create and compare. On every state change, React re-runs your components to build a new virtual tree, and now it faces a question: the real DOM currently matches the old tree, so what's the smallest set of real changes to make it match the new one? Answering that efficiently is reconciliation, and it's the engine that lets you program as if you rebuild everything while React quietly does almost nothing. The whole framework rests on this: describe the target, let React compute the diff, and keep the expensive real-DOM mutations to a minimum.

The one-sentence version

You return a new virtual DOM on every render; React reconciles it against the previous tree to apply minimal real changes — and it matches list items by key: same key = reuse in place, new key = mount, missing key = unmount.

02

Reconciliation

Reconciliation is the diffing process. React holds the previous virtual tree, builds a new one when state changes, and walks both to find what actually differs — then applies only those changes to the real DOM. The reason this is a win is a cost asymmetry: creating and comparing plain objects in memory is cheap, while mutating the real DOM (and triggering layout/paint) is expensive. So React does the cheap thing a lot to avoid doing the expensive thing.

To keep the diff fast, React uses heuristics rather than a perfect (and slow) tree-diff. Two matter most. First, different element types produce different trees: if a node changes from a <div> to a <span>, React doesn't try to reconcile them — it tears down the old subtree and builds the new one. Second, and the focus of this handbook: lists are matched by key. When React reconciles a list of children, it doesn't compare them purely by position — it pairs old and new items by their key prop, which is how it knows that an item which moved is still the same item and should be reused rather than rebuilt. Everything about efficient list updates flows from that key-matching step.

03

Keys are identity

A key is the identity React assigns to a list item so it can track that item across renders. During list reconciliation, React pairs old and new children by key and takes one of three actions:

Reconciling a list by key
key in bothReuse — same item; update it in place, keep its DOM node and component state.
key new onlyMount — a fresh element is created and inserted.
key old onlyUnmount — the element is removed and destroyed.
reorderSame keys, new order → everything reused (moved), zero mount/unmount.

The payoff of stable keys is that reordering a list is nearly free: if the keys are the same, React knows every item still exists and just moves the existing DOM nodes, preserving their state (a focused input stays focused, a running animation keeps running). Inserting one item mounts exactly one node; removing one unmounts exactly one; the rest are untouched. This is why React requires a key on list items — it's not a formality, it's the identity information the diff needs to reuse elements instead of blowing them away. Give it a stable, data-derived key (a database id, a slug) and reconciliation does the minimum possible work. The next sections show what happens when the key is wrong.

04

The diff math

Diffing a list by key partitions the items into three sets — reused (in both), mounted (new only), unmounted (old only):

reused = new ∩ old    mounted = new ∖ old    unmounted = old ∖ new

Real-DOM cost is the mounts + unmounts; reused items only update in place. So a pure reorder (same key set) costs 0 mount/unmount ops — everything is reused.

The expensive operations are exactly the mounts and unmounts; a stable key set makes a reorder free, an index key makes it costly and buggy:

dom_ops  =  |mounted| + |unmounted|     reorder with stable keys ⟹ 0     index key ⟹ wrong reuse (state leaks)

Reorder ["a","b","c"]→["c","b","a"]: same keys, 0 DOM ops. The runnable version below computes the diff, the DOM-op cost, and detects when an index key breaks.

RUN IT YOURSELF

Reconciliation by key

React reconciles a list by matching items by key: a key in both the old and new list means reuse the element in place (keeping its DOM node and state); a key only in the new list mounts a new element; a key only in the old list unmounts one. The real cost is the mounts plus unmounts — so a pure reorder with stable keys costs zero. Using the array index as a key breaks this: on a prepend or reorder, an index points at different data than before, so React reuses the wrong element and state leaks to the wrong row. Change the key lists and watch the diff and the cost.

CPython · WebAssembly
05

The index-key trap

The most common React bug of all comes from using the array index as the keyitems.map((item, i) => <Row key={i} …/>). It looks harmless and often works in demos, because the index is unique. The problem is that an index describes a position, not an identity. When the list is reordered, filtered, or gets an item inserted at the front, positions shift but the index keys don't move with the data.

Concretely: prepend an item and the row that was at index 0 is now at index 1 — but React still sees key=0 at position 0 and thinks it's the same element as before, so it reuses that DOM node in place with new data. Any state living in that element — the text typed into an input, which checkbox is checked, a focus ring, an in-progress animation — stays with the position instead of moving with the item. You type into the first row, insert a row above it, and your text is now attached to the wrong entry. These bugs are maddening precisely because the render looks right and only the state is wrong. The runnable model above detects exactly this: when position i's identity changes while its index key stays the same, React's reuse is silently incorrect. Index keys are safe only for a static list that never reorders, filters, or inserts anywhere but the end — otherwise, use a stable unique id from your data, and the whole class of bug disappears.

06

Pitfalls

Beyond the index-key trap, the biggest reconciliation-adjacent pitfall is unnecessary re-renders. When a component's state changes, React re-renders it and its children by default, rebuilding their virtual trees (then reconciling). That's usually cheap, but for expensive subtrees it adds up — and a common cause is passing a freshly-created object or function as a prop on every render, which makes memoized children think something changed. The tools are React.memo (skip re-rendering a child whose props are unchanged), useMemo/useCallback (keep the same reference across renders), but reach for them when you've measured a problem, not reflexively.

Two more. Keys that aren't stable: a key generated fresh each render (key={Math.random()}) is as bad as no key — every item looks new, so React unmounts and remounts the whole list every render, destroying state and performance. Keys must be stable and tied to the item's identity. And treating the virtual DOM as inherently fast: it isn't magic — it's a diffing buffer whose whole job is to minimize real-DOM work, and you can defeat it (with unstable keys, giant unmemoized trees, or churny props) into doing more work than hand-written DOM code. Respect what it's doing and it's a huge win; fight it and it's overhead. The whole handbook reduces to the mental model worth keeping: you describe the UI, React diffs your description against the last one and applies the minimum, and keys are the identities that let that diff reuse elements — so give every list item a stable, data-derived key and reconciliation does almost no work at all.

Worth knowing

Give list items a stable, data-derived key (never the array index for dynamic lists, never a random value). Reach for React.memo/useMemo/useCallback only when you've measured an expensive re-render. The virtual DOM minimizes real-DOM work — it isn't automatically fast, and unstable keys defeat it.

Frequently asked

Quick answers

What is reconciliation?

React diffs a new virtual DOM tree against the previous one and applies only the minimal real-DOM changes needed to match it.

What is the virtual DOM?

A cheap in-memory description of the UI that React diffs against the last render — a buffer that lets it minimize expensive real-DOM mutations.

Why do lists need keys?

Keys are the identity React uses to match list items across renders — reuse on match, mount on new, unmount on gone — so it can move elements instead of rebuilding them.

Why is index-as-key bad?

An index is a position, not an identity; on reorder or insert, React reuses the wrong element and state leaks to the wrong row. Use a stable data id.

Finished this one? 0 / 99 Handbooks done

Explore the topic

See this alongside everything else on the same subject — handbooks, system designs, challenges and tools, in one place.