TL;DR / Reconciliation is React's process for comparing the previous and current virtual DOM trees to determine the minimal set of DOM mutations needed to update the UI.
How It Works
Previous Tree: Current Tree:
┌───────┐ same type ┌───────┐
│ div │──────────────────────────→│ div │
└───────┘ └───────┘
│ │
child│ child child│child
┌────└───────┐ ┌─────└─────┐
│ │ │ │
↓ ↓ ↓ ↓
┌──────┐ ┌────────┐ ┌──────┐ ┌───────┐
│ <p> │ │ <span> │ │ <p> │ │ <div> │
└──────┘ └────────┘ └──────┘ └───────┘
type changed = remount
Same type = update props. Different type = unmount + mount.
Keys let React match children across reorders.
When state changes in a React application, React does not re-render the entire DOM. Instead, it builds a new virtual DOM tree from the updated component tree and compares it to the previous virtual DOM tree. This comparison process — reconciliation — produces a minimal list of DOM operations (insertions, deletions, updates) that transform the real DOM from its current state to the desired state.
The Two Heuristic Assumptions
The general tree edit distance problem is expensive. Classic algorithms run in O(n^3) time; later improvements like Zhang-Shasha brought this down to O(n^2) for ordered trees. For a tree with 5,000 elements, even O(n^2) means 25 million comparisons per render — far too slow for 60fps. React reduces this to O(n) by making two assumptions:
-
Different element types produce different trees. If a
<div>becomes a<span>, React does not try to morph one into the other. It unmounts the entire<div>subtree (destroying component instances and DOM nodes) and mounts a fresh<span>subtree. This avoids expensive cross-type diffing. -
Keys identify stable elements across renders. Without keys, React matches children by position. With keys, React matches children by key value, enabling efficient handling of reorders, insertions, and deletions in lists.
These heuristics are imperfect — there are cases where a type change preserves DOM structure, or where positional matching would be correct. But the heuristics produce correct results in the overwhelming majority of real-world cases and reduce algorithmic complexity by orders of magnitude.
Element-Level Comparison
Reconciliation starts at the root and walks the tree depth-first. At each node, React compares the previous and current elements:
Same type (DOM element): React keeps the DOM node, diffs the attributes, and updates only the changed ones. It then recurses into the children. For example, changing <div className="old"> to <div className="new"> updates the className attribute without touching the DOM node itself.
Same type (component): React keeps the component instance, calls it with the new props, and reconciles its returned element tree. State and refs are preserved. Lifecycle methods or effects for prop changes fire.
Different type: React unmounts the old subtree entirely — component instances are destroyed, useEffect cleanups run, DOM nodes are removed. Then it mounts the new subtree from scratch. This is the most expensive operation and why changing element types at the same tree position should be avoided.
The Key Algorithm for Lists
When reconciling children of a parent element, React processes them differently depending on whether keys are present.
Without keys (index-based matching): React compares children by position. The first old child is compared to the first new child, second to second, and so on. If an item is inserted at the beginning of a list, every subsequent item appears to have "changed" because its position shifted. React updates every item instead of just inserting one.
With keys: React builds a map of old children keyed by their key prop. For each new child, it looks up the corresponding old child by key. If found, it moves and updates it. If not found, it creates a new element. Old children not found in the new list are deleted. This reduces a list reorder from O(n) updates to O(n) moves, which are much cheaper DOM operations.
The key algorithm uses a two-pass approach: first, it processes new children left-to-right, matching them to old children by key. Items that match and are in the same relative order are updated in place. Items that need to move are marked for reordering. This minimizes the number of DOM insertBefore operations.
Batching and Commit Phase
Reconciliation does not touch the DOM during the comparison phase. It produces a list of "effects" — DOM mutations to apply. These effects are committed in a separate phase, the commit phase, which applies all DOM changes synchronously. This separation ensures the user never sees a partially updated DOM.
In concurrent mode, the reconciliation (render) phase is interruptible, but the commit phase is always synchronous. React can pause and resume the comparison work, but once it starts applying DOM mutations, it finishes them all in one synchronous batch.
Gotchas
- Changing element type at the same position destroys all state below it. Wrapping a component in a
<div>that sometimes becomes a<section>forces a full remount of all children, losing their state and triggering expensive re-creation. - Array index as key is almost always wrong for dynamic lists. It produces the same behavior as no key at all. Use stable, unique identifiers from your data.
- Keys must be unique among siblings, not globally. Two lists in different parts of the tree can safely use the same keys. Keys are only compared within the same parent.
- Reconciliation runs on every render, not just state changes. Parent re-renders cause child reconciliation even if the child's props did not change. Use
React.memoto skip reconciliation for unchanged subtrees. - Fragments and arrays affect sibling matching. Switching between
<Fragment>and a wrapper<div>changes the tree structure and can cause unexpected remounts of child components.