The Reconciliation Algorithm: Type, Keys, and Identity
Learn precisely how React's reconciliation algorithm walks the element tree, compares component type and key at each position, and decides whether to update a component in place (preserving state), remount it (resetting state), or reorder existing instances.
Every time a component re-renders, React must decide, for every element it produced, whether that maps to an existing DOM node/component instance it can update, or whether it needs to be built from scratch. Reconciliation is the algorithm behind that decision, and it runs entirely on the structure of your element tree — never on deep equality of your actual data.
The element tree and what 'identity' means
Every render, a component function returns a tree of React elements — plain objects with a type (a tag name string like 'div', or a component function/class) and props (including any key). React keeps this tree from the previous render (the 'current' tree) and compares it, position by position, against the freshly rendered tree.
'Identity' in React's reconciliation sense means: does this position in the tree represent the same logical component instance as before? Identity is not about whether the data looks the same — a component can have completely different props than last time and still be 'the same instance' as far as React is concerned, as long as it's at the same tree position with the same type and key. Conversely, a component with visually identical output can be treated as a brand-new instance if its type or key differs.
This distinction matters because component instance identity is what state is attached to — useState's state, refs, and any internal instance fields live on a Fiber that persists only as long as React considers it 'the same instance.' Losing identity means losing state, even if nothing about the visible output changed.
The comparison algorithm at each position
At each position in the tree, React's algorithm follows two simple rules: (1) if the element type differs from what was there before (a <div> becomes a <span>, or ComponentA becomes ComponentB), React tears down the entire old subtree — unmounting components, discarding their state, removing their DOM nodes — and mounts a completely new subtree from scratch, even if the new type produces near-identical output; (2) if the element type is the same, React keeps the existing DOM node or component instance, updates its props in place, and recurses into its children using the same two rules.
For a list of children specifically, React additionally uses key to match old children to new children when the list itself might have been reordered, filtered, or had items added or removed. Without keys, React falls back to matching children purely by their index/position in the array, which silently breaks the moment the list order changes — an item that moved from position 2 to position 0 gets compared against whatever unrelated element used to be at position 0, not against its own previous self.
With explicit, stable keys, React can correctly recognize 'this exact logical item, now at a new position' regardless of reordering, and it moves the existing DOM node and preserves that item's component state rather than destroying and recreating it.
State preservation vs. state reset
State is preserved exactly when React judges an element to be 'the same instance' as before: same type, same position (or same key within a keyed list), same parent. This is true even across renders where the props passed to that component changed substantially — a Counter component keeps its internal count as its label prop changes, because it's still recognized as the same instance.
State is reset (the component unmounts and a fresh instance mounts) in three main situations: the element's type changes at that position; the element's key changes, even if the type stays the same; or the element is removed from the tree entirely (conditionally rendered away) and later re-added, which is treated as a brand-new mount, not a resumed one.
This gives you a deliberate tool: changing a component's key is a common, intentional technique to force a full remount — and therefore a full state reset — when you want one. For example, giving a form component key={userId} ensures that switching from editing one user's data to another's discards any leftover draft state instead of the new user's form inheriting stale field values from the previous user.
Forcing a remount via key
Changing key when selectedUserId changes forces React to unmount the old UserForm instance and mount a fresh one, resetting all of its internal state. Without the key, UserForm stays 'the same instance' across the userId change, and any local state (draft text, validation errors) would carry over incorrectly.
// Editing user A, then switching to user B -- without a key change,
// the SAME UserForm instance is reused, so any local draft state
// (e.g. an uncommitted text field) would incorrectly persist across users.
<UserForm key={selectedUserId} userId={selectedUserId} />Code Examples
Even though Counter and FancyCounter look and behave almost identically, they are different component types. Toggling fancy causes React to unmount whichever one was active and mount the other as a brand-new instance, resetting count back to 0 -- reconciliation compares type, not visual or behavioral similarity.
return fancy ? <FancyCounter /> : <Counter />;
Common mistake: Assuming two components that render 'basically the same thing' will preserve state across a swap between them -- reconciliation only preserves state when the type itself is identical at that position.
Because key={index} makes identity depend on position rather than on the todo's own identity, removing an item causes every subsequent item to be matched against the wrong previous instance, dragging that instance's state along with it incorrectly. Expected result: Expected: only the deleted row disappears. Actual (with index keys): visible rows may keep the wrong 'editing' or 'expanded' state after a delete.
function Counter() {
const [count, setCount] = useState(0);
return <button onClick={() => setCount((c) => c + 1)}>{count}</button>;
}
function FancyCounter() {
const [count, setCount] = useState(0);
return <button onClick={() => setCount((c) => c + 1)} style={{ fontWeight: "bold" }}>{count}</button>;
}
function Parent({ fancy }: { fancy: boolean }) {
// Same position, but the TYPE differs between renders when `fancy` toggles.
return fancy ? <FancyCounter /> : <Counter />;
}function TodoList({ todos }: { todos: { id: string; text: string }[] }) {
return (
<ul>
{todos.map((todo, index) => (
// BUG: key={index} ties identity to POSITION, not to the todo itself
<TodoRow key={index} todo={todo} />
))}
</ul>
);
}
// After deleting the first todo, every remaining todo shifts up one index.
// React matches each new index against the OLD element that had that same
// index -- so TodoRow's internal state (e.g. "isEditing") ends up attached
// to the wrong todo.Decision at each tree position
Reconciliation never inspects your data's meaning -- only type and key at each position decide identity.
Same type + same key -> UPDATE in place (state preserved)
Same type + new key -> new instance (state reset)
Different type -> unmount + remount (state reset)
Element removed, then
later re-added -> fresh mount (state reset, not resumed)Important Rules
Reconciliation compares elements position by position, using type first and key second to decide identity.
A type change at a given position always unmounts the old subtree and mounts a new one, discarding all of its state.
A key change at the same type and position is also treated as a new instance -- it deliberately resets state.
In lists, use a stable, unique identifier (never the array index) as the key whenever the list can be reordered, filtered, or have items inserted or removed.
Changing a component's key on purpose is the standard technique for forcing a full remount/state reset when you want that behavior.
Where this shows up
A tabbed settings panel that swaps between 'Profile', 'Billing', and 'Security' forms benefits from keying each tab's form by its tab id -- this guarantees that switching tabs never leaves a stray draft value from one tab bleeding into another, because each tab's form is a genuinely distinct instance to React, not a reused one with swapped props.
items.map((item, i) => <Row key={i} data={item} />) items.map((item) => <Row key={item.id} data={item} />)
Index-based keys tie a component's identity to its position in the array. The moment the array is reordered, filtered, or has an item removed, positions shift and React matches new items against the wrong previous instance's state.
// Trying to reset a form's fields by calling a reset() method imperatively formRef.current?.reset();
<UserForm key={selectedUserId} userId={selectedUserId} /> // changing selectedUserId naturally remounts the form with fresh state
Rather than building imperative reset logic, changing the key when the underlying identity (e.g. which user is being edited) changes lets React's own remount behavior handle the reset declaratively and consistently.
Two sibling elements at the same tree position change from <ComponentA/> to <ComponentB/> across a re-render. What does React do? A. It diffs their props and updates ComponentA in place to look like ComponentB B. It unmounts ComponentA (discarding its state) and mounts a fresh ComponentB instance C. It keeps both mounted simultaneously D. It throws an error because the type changed
B. It unmounts ComponentA (discarding its state) and mounts a fresh ComponentB instance — A type change at a given position is always treated as a completely different identity -- React tears down the old subtree, including all of its state, and mounts the new type as a brand-new instance.
Why does using the array index as a key cause problems specifically when a list can be reordered or filtered? A. Because array indexes are not valid JavaScript values for props B. Because index-based keys tie component identity to position, so when items shift position, React matches new items against the wrong previous instance's state C. Because React requires keys to be numbers, and index already satisfies that, so there's no real problem D. Because it makes the initial render slower
B. Because index-based keys tie component identity to position, so when items shift position, React matches new items against the wrong previous instance's state — Keys are how React tracks identity independent of position for list children. Index keys defeat that purpose, since the index itself changes whenever the list is reordered or an item is added/removed before a given item.
What is the standard technique for intentionally forcing a component to fully remount, resetting all of its state? A. Call component.forceUpdate() from outside B. Change the value passed as that component's key prop C. Wrap it in React.memo D. Change one of its regular (non-key) props
B. Change the value passed as that component's key prop — Since a changed key at the same type and position is treated by reconciliation as a new identity, deliberately changing the key is the idiomatic way to force React to discard the old instance and its state and mount a fresh one.
A chat app renders a list of messages with key={index}. Users report that after a new message arrives at the top of the list (newest-first ordering), an open 'reply' input on an unrelated older message appears to jump to a different message. Diagnose the cause and describe the fix.
Inserting a new message at the front shifts every existing message down by one index. Because identity is keyed to index, React matches each shifted message's new index against the previous element that held that index -- meaning the local 'reply open' state that belonged to the element previously at index 2 is now attached to whatever message currently occupies index 2, which is a different message after the insert. The fix is to key each message by its own stable message id (key={message.id}), so identity travels with the message itself regardless of where new items are inserted.
Think about how inserting an item at the front of an index-keyed list shifts every other item's index.