Keys in lists are identity, and using the index breaks it
When you render a list, the key is not a formality to silence a warning — it is
how the framework answers “which item is which” across two renders. Between the old
list and the new one, React matches elements by key to decide what moved, what was
added, and what was removed, then reuses the DOM and component state accordingly.
Give each item a stable identity (its id) and reordering, inserting, and
deleting all just work. Use the array index as the key and you have told React
that identity is position — so when positions change, it attaches the wrong
state to the wrong row, and you get lost input, misfired animations, and checkboxes
that follow the slot instead of the item.
The index says “identity is position”
Here is the bug in its natural habitat. The list has editable rows, and it uses the index as the key:
// index key: React thinks row 0 is always "the same" element
{items.map((item, i) => (
<li key={i}>
<input defaultValue={item.name} /> {/* DOM state keyed to POSITION */}
</li>
))}
Prepend a new item and every existing item shifts down a slot. React, matching by key, believes the element at index 0 is unchanged — so the text you typed, the focus, and the DOM node all stay at index 0, now showing a different item’s data. Nothing looks obviously wrong until a user notices their input jumped rows.
A stable id says “identity is the item”
Give each row its real id and the matching becomes correct: React finds “item #a1” in both renders, knows it merely moved, and moves its DOM and state with it:
// id key: React tracks each item by its true identity across reorders
{items.map((item) => (
<li key={item.id}>
<input defaultValue={item.name} /> {/* state stays with the ITEM */}
</li>
))}
Now inserting, deleting, sorting, and filtering all preserve per-row state and animations, because the key means what React assumes it means.
When the index is actually fine
The nuance worth keeping: an index key is harmless when the list is static — never reordered, never filtered, never inserted into except at the end, and its items hold no per-element state (no inputs, no local toggles, no animations). A render-once list of read-only labels can key by index without consequence. But the moment any of those conditions might change, the index is a latent bug, and “we might sort this later” is common enough that a stable id is the safe default. The rule: key by whatever is the item’s identity — a database id, a slug, a uuid — and reach for the index only for a provably static, stateless list. The data-table-sort exercise is the fastest way to feel this, because sorting is exactly the operation that turns an index key from “fine” into “why did my selection jump.”