Reconciliation investigation
Delete the first row of a list and the wrong checkbox stays ticked. The list is correct. The keys are not.
- Level
- ENGINEER
- Read time
- 09 min
- Experiment
- Available
- Type
- INTERACTIVE
The question
Why does using an array index as a key cause state to attach to the wrong item, while a stable id does not?
Hypothesis
Keys identify an element between renders. With index keys, removing an item shifts every subsequent identity by one, so React reuses the wrong component instance and its state travels to a different row.
Method
Laboratory available
The instrument for this investigation runs in the laboratory, where the controls, the live model and the observation log share one workstation.
Enter the laboratoryTwo identical lists, one keyed by index and one by id. Type into a row, then delete a row above it.
What we observed
The id-keyed list keeps every input attached to its own row. The index-keyed list drags the text upward: the instance that used to be key=1 is reused for whatever is now at index 1.
Why it happens
Reconciliation compares the new element tree with the previous one, one level at a time, using two heuristics: elements of a different type produce a full remount, and elements with the same key are assumed to be the same logical item. This is what makes the diff linear instead of a general tree-edit-distance problem.
// Identity shifts when the list mutates{rows.map((row, i) => <Row key={i} row={row} />)}// Identity follows the data{rows.map((row) => <Row key={row.id} row={row} />)}| Key strategy | Safe when |
|---|---|
| Stable domain id | Always preferred |
| Array index | The list is static, never reordered, filtered or spliced |
| Math.random() | Never. Every render remounts every row and destroys state and focus |
Composite ${a}-${b} | Fine, as long as the composite is unique and stable |
The same mechanism can be used deliberately. Changing a key is the documented way to reset a component's state, for example <Form key={userId} /> gives each user a clean form without a manual reset effect.
Further research
Under the Fiber architecture, reconciliation happens during the render phase, which is interruptible: React can pause, abandon or restart it. The commit phase, where the DOM is actually mutated, is synchronous and cannot be interrupted, which is why side effects belong in effects rather than in render.
References
- 01React docsPreserving and resetting state
- 02React docsRendering lists, keys