key={index} Will Bite You: A Field Guide to React Reconciliation
The bug report said 'I deleted one row and a different row's data changed.' The cause was one character: using the array index as a key. Here's what's actually going on.
The bug report was one sentence: "I deleted the second attendee and the third attendee's dietary notes disappeared."
Now that's an interesting sentence. Data doesn't just hop between rows. Except, in React, sometimes it does — and when it does, I'd bet money on what's in the code before I even open the file:
{attendees.map((attendee, index) => (
<AttendeeRow key={index} attendee={attendee} />
))}
key={index}. The most innocent-looking bug in frontend development. It renders correctly, it silences the console warning, it passes every test that doesn't mutate the list. And then a user deletes a row and someone else's data goes sideways.
What keys are actually for
To understand why, you need one piece of mental furniture: React doesn't "move" DOM nodes around to match your data. On each render, it compares the new tree of elements to the previous one and asks, for every child in a list: is this the same conceptual thing as before, or a different thing?
The key is the answer to that question. It's an identity claim. key="attendee-42" says "this element is attendee 42 — however the list gets reordered, filtered, or trimmed, match it up with the previous render's attendee 42 and preserve its state."
When identities match, React keeps the existing component instance: its useState values, its uncontrolled input contents, its focus, its scroll position, its in-flight animations. When identities don't match, React tears the old instance down and mounts a fresh one.
key={index} makes the identity claim "this element is position 2." Which is fine — right up until position 2 stops meaning the same attendee.
Anatomy of the hopping-data bug
Walk through the delete. You have three attendees, and each AttendeeRow keeps some local state — say, an expanded notes textarea the user typed into but hasn't saved:
- Before:
key=0→ Alice,key=1→ Bob,key=2→ Carol (Carol's row has unsaved notes in local state). - User deletes Bob. New array:
[Alice, Carol]. - After:
key=0→ Alice,key=1→ Carol.
React's view: "key 0 still exists, keep it. Key 1 still exists, keep it — just new props. Key 2 is gone, unmount it."
So the component instance that used to render Bob now receives Carol as props — while keeping Bob's row's local state. Carol's row shows state that belonged to the row formerly at her position. Meanwhile the instance that actually held Carol's unsaved notes (key 2) got unmounted, taking her notes with it. The user's report — "a different row's data changed" — is a perfectly accurate description of reconciliation doing exactly what the keys told it to.
The same mechanism produces a whole family of symptoms: checkboxes that stay checked on the wrong row after sorting, input focus jumping when a list prepends an item, animations firing on rows that "didn't change," and stale memoized values sticking to positions instead of records.
Choosing a real key
The rule is short: the key should be a stable, unique identifier of the data, not of the position.
// Good: identity travels with the record
{attendees.map((attendee) => (
<AttendeeRow key={attendee.id} attendee={attendee} />
))}
Some practical notes from the review trenches:
- Server IDs are the gold standard. If the record came from a database, it has one. Use it.
- Client-created items need client IDs. When the user adds a row that hasn't been saved yet, generate an id at creation time (
crypto.randomUUID()) and store it on the item. Don't wait for the server. - Never generate keys during render.
key={Math.random()}orkey={uuid()}inside the map is worse than the index — every render unmounts and remounts every row, nuking state and performance together. - Composite keys are fine. No natural id?
key={`${item.userId}-${item.date}`}works if the combination is genuinely unique and stable. - Names, labels, and titles are not keys. Two attendees named "Sam," or one attendee getting renamed, and you're back in hopping-data territory.
When is key={index} actually okay?
Being fair: if the list is static — never reordered, never filtered, items never inserted or removed, and rows hold no state — the index is harmless. A hardcoded list of four feature bullets? Fine. But notice how many conditions that sentence carried. Lists have a way of growing sorting and deleting six months after they're written, and the key doesn't announce that it's now a bug. My default in review: flag it unless the list is obviously and permanently static, and even then, if a real id is sitting right there on the object, just use it.
Why this keeps showing up in PRs
Partly it's that the failure is invisible at write time — the list renders perfectly. Partly it's muscle memory from tutorials. And increasingly, it's AI-generated code: assistants produce key={index} constantly, because an enormous share of their training data does. I've reviewed AI-written components that were otherwise genuinely good — nice types, sensible structure, handled the empty state! — with key={index} sitting in the middle like a land mine. The assistant doesn't know your list will be user-sortable next sprint. The person reviewing needs to.
There's a second AI-flavored variant worth knowing: assistants sometimes "fix" the duplicate-key console warning by switching to key={Math.random()}. The warning disappears, and now every keystroke in any row remounts the entire list. If a diff makes a key warning vanish, check how.
One more place keys earn their keep
Keys aren't only for lists. Because a changed key means "different thing — remount it," you can use a key to deliberately reset a component's state:
// Reset the entire form when switching users — no effects needed
<ProfileForm key={selectedUserId} userId={selectedUserId} />
This replaces the fragile pattern of useEffect watching a prop and manually resetting a dozen state values. One attribute, and React tears down and rebuilds the form with fresh state. It's one of my favorite "delete ten lines, fix the bug" review suggestions.
The short version
- A key is an identity claim, and React preserves or destroys component state based on it.
key={index}claims "identity = position," which breaks the moment items are removed, inserted, or reordered — state stays at the position while data moves.- Use stable ids from the data; mint client-side ids at creation time for unsaved items.
- Never generate keys during render, and treat
key={Math.random()}as a red flag, not a fix. - In review, assume every list will eventually be sorted, filtered, or edited — because it will.
- Bonus: a deliberate
keychange is the cleanest way to reset a component's state.
One character of difference in a diff. An afternoon of debugging on the other end. That's exactly the kind of trade review exists to catch.