When a React parent re-renders a list of children, how does the reconciler decide which fiber from the previous render corresponds to each new element — with keys and without them?
answer
- two sequences, one question per element
- position is the fallback identity
- fast parallel walk, then a map
- key compared before element type
- leftovers in the map get deleted
basics
~20 sWithout keys, React pairs children by position: slot one with slot one. With keys, it pairs by key, walking both lists in parallel while the keys agree and then looking up the remaining previous children in a key-based map.
solid answer
~50 sReact reconciles a parent's children as two lists. Without keys the pairing is purely positional — the element at index 2 is matched with whatever fiber sat at index 2 last time. With keys, React first walks both lists in parallel while the keys line up, which covers the common case of an unchanged list or an append cheaply. At the first key mismatch it stops, collects the remaining previous children into a map keyed by their keys, and looks up each remaining new element there. A hit reuses that fiber, possibly marking it to be moved in the DOM; a miss creates a new fiber; anything left in the map at the end is deleted. Crucially the key is checked *before* the element type, so a matching key with a different type still means no reuse. Reuse means the existing fiber and its hook state and DOM node are kept and updated in place.
go deeper
Recall that keys tell React which element is which between renders, and that with no key React falls back to matching by position in the list.
Walk the pairing explicitly: parallel walk while the keys agree, then a map of the remaining previous children, hits reused, misses mounted, leftovers deleted. Say that the key is compared before the element type.
Connect the mechanism to what survives: a reused fiber keeps its hook state, refs, DOM node, focus and uncontrolled values, and a move is an insertion rather than a rebuild. Use that to explain real list bugs you have debugged.
Own the tradeoff behind the design: React chose a linear heuristic with a caller-supplied identity hint over an exact tree diff. Be ready to argue what that buys, and where handing identity to the caller becomes a liability at scale.
## The problem the reconciler is solving Rendering a list gives React two flat sequences of elements: the children this parent produced last render, and the children it just produced. It has to answer one question per new element — "which of the old ones are you?" — because the answer decides whether an existing fiber (with its hook state, its refs, its real DOM node) is updated in place, or thrown away and rebuilt. A true minimal-edit-distance tree diff is too expensive to run on every keystroke, so React uses a linear pass with one hint from you: the key. ## Without keys: pairing by position If the children carry no key, React treats position as identity. New child at index *i* is paired with the previous child at index *i*. If they are the same element type, the fiber is reused and its props are updated; if the type differs, that fiber is deleted and a new one is created for the slot. Extra new children at the end are mounted; leftover previous children at the end are deleted. This is exactly what array-index keys do, which is why index keys and no keys behave identically apart from the development warning. ## With keys: pairing by name With keys, the pass has two stages. **Stage one — walk in parallel.** React steps through both lists together. As long as the new child's key equals the old child's key at the same position, it pairs them immediately. For a list that did not change, or that only had items appended or had props updated in place, the whole reconciliation is this cheap linear walk and nothing else runs. **Stage two — the map.** At the first position where the keys disagree, the parallel walk stops, because from here on positions no longer correspond. React puts every remaining previous child into a map keyed by its key (children without a key are stored under their index). Then it continues through the remaining new elements, looking each one up in that map: - **Hit** — the previous fiber is pulled out of the map and reused. React also tracks how far along the previous list it has already placed things; if this fiber came from an earlier position than that watermark, it is flagged so the commit phase moves its DOM node with an insertion, rather than recreating it. - **Miss** — no previous child owned that key, so a new fiber is created and mounted. Whatever is still in the map when the new list is exhausted was not claimed by anyone, so those fibers are deleted: their effect cleanups run, their refs are detached, their DOM nodes are removed. ## Key is checked before type This ordering surprises people. React first asks "do the keys match?" and only then "is it the same element type?". Two consequences: - Same key, different type (`<Row key="a" />` becomes `<Card key="a" />`): the fiber is not reused. The old one is deleted, a new one is mounted. - Different key, same type: also no reuse. The key mismatch alone is enough to discard the fiber, even though the component is identical. ## What "reuse" actually means Reusing a fiber is not a shallow optimisation — it is what makes a component feel continuous: - the fiber's hook state list survives, so `useState` values, `useRef` boxes and reducer state are exactly what they were; - the host DOM node survives, so uncontrolled input values, focus, text selection, scroll position and running CSS transitions survive; - effects are not torn down; they re-run only if their dependencies changed. Moving a reused fiber is cheap too: the commit phase performs an insertion of the existing node at the new position. Nothing is re-created. ## Why the hint has to come from you React cannot infer identity from the data — it never sees your objects, only elements. The key is the one place you tell it "this element is the same thing as the one that had this name last time". Everything else in list reconciliation follows mechanically from that one input, which is why a key derived from something unstable poisons the whole process. ## Answering well Name the two stages (parallel walk, then map lookup), say that the key is compared before the type, and finish with what reuse buys — fiber state and DOM node preserved, versus delete-and-mount when nothing matches.
- Why does React bother with the parallel walk instead of going straight to the map?Because the overwhelmingly common cases — an unchanged list, or one with items appended, or one where only props changed — resolve entirely in that walk with no allocation. The map is built only from the point where the two lists stop agreeing, so the extra cost is paid only by lists that were actually reordered or spliced.
- When a reused fiber has to change position, does React recreate its DOM node?No. The fiber is flagged for placement, and the commit phase inserts the existing DOM node at its new position. The node, its uncontrolled input value, and any attached listeners are preserved — moving is an insertion, not a rebuild.
- What happens when the key matches but the element type has changed?No reuse. React compares the key first and the type second, so a matching key gets you as far as the type check; a different type then means the old fiber is deleted — cleanups run, refs detach — and a fresh fiber is mounted for the new type with new state.
saying these in an interview costs you the question
- React diffs the real DOM trees to find the changes
- Keys let React skip re-rendering unchanged children
- Without keys React falls back to comparing props
- Matching keys reuse the fiber even when the type changed
- Reordering a keyed list recreates the DOM nodes