Every React developer has written this line, and most lists in production still carry the bug:
{items.map((item, i) => <Row key={i} item={item} />)}It renders fine. That is what makes it dangerous. The list looks correct until someone inserts a row, sorts a column, or filters while an input has focus. Then state starts wearing the wrong item's name, and nothing in the console ever complains.
Step 1: Give React identity, not position
When a list re-renders, React matches old children to new children by key. With a stable key, "item 42 moved from row 1 to row 3" is a DOM node that moves. With an index key, React sees "row 1 now holds different data" and patches the node at position 1 in place, keeping its DOM, its state, and its scroll position.
Keys answer one question: is this the same thing as last time? Index keys answer "is this the same slot," which is a different question with different bugs.
Step 2: Never use the index for mutable or reorderable lists
The failure demo I show in code reviews fits in ten lines:
// three rows with inputs, keyed by index
// state: ["a", "b", "c"], text typed in row 2: "hello"
items.splice(0, 1); // remove "a"
// row 1 now renders "b" but keeps the input holding "hello"The input state did not move. React thinks row 1 is still row 1, so the uncontrolled input keeps its value while item changed underneath. Sort the list and every focused input, every animation, every half-filled form scrambles. If a list can ever change shape, index keys are incorrect, not just suboptimal.
Step 3: Use a stable domain ID
{items.map((item) => <Row key={item.id} item={item} />)}Database ID, UUID, slug, composite string. The requirements: unique within the list, stable across renders, and derived from the item itself so sorting and filtering preserve identity. When the backend has no ID, hash the fields that define the item or demand an ID from the backend. Silently counting from zero is how the bug got in.
Step 4: Do not generate keys during render
The worse twin of index keys:
{items.map((item) => <Row key={Math.random()} item={item} />)}Every render, every key is new, React concludes the entire list is new, and unmounts plus remounts all of it. Inputs lose focus mid-typing, animations restart in a loop, and performance is worse than any missing memo. I have shipped this bug myself, early enough that I now grep for it in every list I touch.
Step 5: Watch the state that moves between rows
How the bug reaches production: a todo list with per-row editing works perfectly in the demo, then a customer sorts by due date and their in-progress edit lands on a different todo. Uncontrolled inputs, <input key={...}> reset tricks, framer-motion layout animations, focus management: all of them bind to the identity the key defines.
The audit is quick. Find every .map( in the codebase, check each key, and ask: if two items swapped positions mid-render, would React know? If the answer is no, the list is one user action away from the bug report.
The rule I actually follow
Static lists that never reorder: index keys are fine and honest. Anything the user can sort, filter, insert into, or delete from: stable domain IDs, no exceptions, no "temporary" index keys that outlive the sprint.
Hunting a bug where list state ends up on the wrong row? That audit takes me an hour. Tell me about the project and I will find which list is lying about identity.