Skip to content

Reconciliation, identity and keys

React pairs each new child with an old one by key, or by position without a key; state stays with the paired old child.

Predict first

Before you read: predict

ExamHall candidate app. A section shows Q2, Q5 and Q7. Each QuestionCard keeps the candidate's draft in its own state:

The candidate types 42 into Q2's box. The section is then re-shuffled and renders the questions in the order Q5, Q2, Q7.

Which box shows 42 now?

Code for this question
function QuestionCard({ q }) {  const [draft, setDraft] = useState('');  return <li><label>{q.id}</label><input value={draft} onChange={e => setDraft(e.target.value)} /></li>;}// in the section:questions.map((q, i) => <QuestionCard key={i} q={q} />)
Pick one

The 30-second version

On every render React pairs each new child with an old child of the same parent: by key if given, else by position. A same-type pair keeps the old component's state and DOM and only receives new props. An unpaired or retyped old child is destroyed; an unpaired new child starts fresh. An index key is just the position.

React pairs each new child with an old one by key, or by position without a key; state stays with the paired old child.

How it works

Step 1 of 8

Pairing happens per parent. On each render React compares a parent's new children with that parent's previous children, never across parents.

Analogy: A hotel bell desk's luggage room (hospitality operations)

Each evening the porter gets a fresh guest list and must hand each guest a stored bag.

In the analogy In React
With no claim tags, the porter hands the first bag on the rack to the first guest on the list No key: pairing by position
A bag keeps its contents; only its outside label is rewritten A kept component keeps its state; only its props change
A claim tag matching the guest's ticket: the bag is found wherever it sits A stable key: a moved item keeps its own state
A "claim tag" that just says "rack spot 1" key={i}: after a re-sort, spot 1's bag goes to a different guest
A ticket with a new tag number matches no bag: the old bag is cleared, the guest gets an empty one A changed key: unmount, then a fresh mount
The spot now expects a garment bag but holds a suitcase: the suitcase is cleared A different type at that position rebuilds the subtree

Breaks down when:

  • A porter can read the name on a bag; React never inspects props to pair children. Wrong inference: "React will notice whose box this is."
  • Claim tags look hotel-wide; keys are unique only among siblings. Wrong inference: "keys must be global IDs."
  • An unclaimed bag goes to lost property; destroyed state is gone. Wrong inference: "switching back restores the old draft."

Contrasting case: the safe-deposit ledger, which logs valuables under the guest's name, not a spot: state lifted into the parent and stored by ID (answers[q.id]). Order and keys cannot move it.

In real terms: reconciliation matches each new element to an existing component instance by key, else by index, and reuses it only when the element type matches too; state lives on the matched instance.

Examples, run for real

E1 gave the same output in headless Chromium with real typing.

E1 · ExamHall EXH-437: "Flagged first" moves answers to the wrong question

Each AnswerBox keeps its draft in state. The candidate types 42 for Q2 and B for Q5, then turns on Flagged first, which moves the flagged Q7 to the top.

list.map((q, i) => <AnswerBox key={i} q={q} />)   // BUG. MR !796: key={q.id}
key={i}: Q7="42" Q2="B" Q5="" | mounts 0, unmounts 0 | Q2's input is the same DOM node: false
key={q.id}: Q7="" Q2="42" Q5="B" | mounts 0, unmounts 0 | Q2's input is the same DOM node: true

Nothing remounts in either run (step 3). With key={i} each draft stays in its slot (step 6); with key={q.id} React moves Q2's own input node (step 5).

E2 · ExamHall: a fresh editor for each attempt

AnswerEditor keeps a draft; the attempt changes from A1 to A2.

<AnswerEditor attemptId={id} />: attempt A2 opens with draft "x = 3" | renders of A2 showing the old draft: 1
<AnswerEditor key={id} attemptId={id} />: attempt A2 opens with draft "" | renders of A2 showing the old draft: 0
useEffect(() => setDraft(""), [attemptId]): attempt A2 opens with draft "" | renders of A2 showing the old draft: 1

Same type, same slot: the draft survives (step 3). The key resets it with no stale render (step 7); the effect clears it one render late.

E3 · Proof domain, chat: position and type decide

The user types to Ana, then opens the group chat.

group ? <Composer to="group" /> : <Composer to="Ana" /> -> "Message group" shows "see you at 6"
group ? <div><Composer to="group" /></div> : <Composer to="Ana" /> -> "Message group" shows ""
{group && <Composer to="group" />}{!group && <Composer to="Ana" />} -> "Message group" shows ""
<Composer key={chatId} to={name} /> -> "Message group" shows ""

Line 1: same type, same slot, so the text would go to the wrong chat (step 3). Line 2 changes the parent's type (step 4). Line 3 uses two slots, since false holds one (step 2). Line 4 resets by key (step 7).

Picture it

a porter in a green uniform lifts a green suitcase with an outlined square tag off a low platform. A blue suitcase with a solid circle tag and a teal suitcase with an outlined triangle tag still stand on the platform, and a dark empty spot marks where the green case stood. Each tag hangs from its own case.
An illustration to help it stick. The mechanism is in the sections above.

Try it

Answer first, then check. Nothing is saved.

Question 1 of 8

(p1-catalog-cart.mjs, 50 runs, 1 distinct output)

A cart renders items.map((it, i) => <CartRow key={i} item={it} />), and CartRow keeps qty in state, starting at 1. The cart holds Mug, Lamp, Desk. The shopper sets Lamp's quantity to 3, then removes Mug.

What do the Lamp and Desk rows show?

Pick one

Question 2 of 8

Same cart.

Which key makes removing Mug leave Lamp at 3 and Desk at 1?

Pick one

Question 3 of 8

(p2-payments-position.mjs, 50 runs, 1 distinct output)

Checkout renders saved ? <BillingForm mode="saved" /> : <BillingForm mode="new" />. BillingForm keeps the billing name in state. The shopper types "Ana Lima", then switches to a saved card.

What does the name field show?

Pick one

Question 4 of 8

Same form, but the saved branch is wrapped: saved ? <div><BillingForm mode="saved" /></div> : <BillingForm mode="new" />. Same typing and switch.

What does the name field show?

Pick one

Question 5 of 8

(p3-streaming-nested.mjs, 50 runs, 1 distinct output)

The viewer raises the volume to 8. One second later t advances.

What does the volume slider show?

Code for this question
function Player() {  const [t, setT] = useState(0);            // advances once a second  function Volume() { const [v, setV] = useState(5); /* slider */ }  return <div><span>{t}</span><Volume /></div>;}
Pick one

Question 6 of 8

(p4-ticketing-key-reset.mjs, 50 runs, 1 distinct output)

SeatPicker keeps the picked seats in state. Fill the slot so the picked seats clear when the event changes, without an effect: <SeatPicker ____ event={ev} />.

Answer in one or two lines.

Question 7 of 8

A support-ticketing board renders two columns, Open and Pending. Each column is its own <ul> and renders tickets.map((t) => <TicketCard key={t.id} ticket={t} />). TicketCard keeps the reply draft in its own useState. An agent types "Refund sent Friday" into ticket #31's reply box, then moves #31 from Open to Pending, and the draft is gone.

Which one change keeps the draft on #31 after the move?

Pick one

Question 8 of 8

A watch page renders <Layout ep={ep}><Player ep={ep} /><Queue /></Layout>. Layout has no state. Player keeps progress and rating in state and renders a <video>; Queue keeps its open tab in state. The viewer is 754 s into episode 4, has rated it 4 and has opened the Trending tab.

Where should key={ep.id} go so that Next episode starts the player at 0 with no rating and leaves the Trending tab open?

Pick one

Go deeper

Pro tips
  1. React does not warn about index keys, so a clean console proves nothing.
    • When it helps: reviewing a list that can be sorted, filtered or have items inserted (step 6).
    • It prints no key: 1 warning(s) | Each child in a list should have a unique "key" prop. and key={i}: 0 warning(s), and duplicate keys give Encountered two children with the same key, …, with the note that non-unique keys "may cause children to be duplicated and/or omitted".
    • Status: RAN (React 19.3.0 development build, Node 24.19.0).
  2. To reset a subtree when an ID changes, put the ID in key, not in an effect.
    • When it helps: a form or editor that must start empty for each record, attempt or chat (step 7).
    • The key line shows 0 renders with the old draft; the effect line shows 1.
    • Status: RAN. Keep key for resets you want. Every key change also recreates the subtree's DOM.
Sources
  • react.dev, "Preserving and Resetting State": https://react.dev/learn/preserving-and-resetting-state. Covers state tied to position in the UI tree; same component at the same position preserves state; a different component at the same position resets its whole subtree; nested component definitions reset state; resetting with a different position or a key; keys specify the position "within the parent" and are not globally unique. Fetched and confirmed; valid as of 2026-09-27.
  • react.dev, "Rendering Lists", keys section: https://react.dev/learn/rendering-lists#keeping-list-items-in-order-with-key. Covers keys unique among siblings, keys must not change, the index being what React uses when no key is given, index keys that "often" lead to subtle bugs, and Math.random() keys that recreate components and DOM and "lose any user input". Fetched and confirmed; valid as of 2026-09-27.
  • Supporting, for E2 and tip 2: react.dev, "You Might Not Need an Effect", Resetting all state when a prop changes: https://react.dev/learn/you-might-not-need-an-effect. The effect approach is "inefficient" because the page "will first render with the stale value". Fetched 2026-09-27.

Checked against the tools and docs as of 2026-09-27.