Keys, lists, and reconciliation
React's warning about a missing key isn't nagging about style — key is the one piece of information reconciliation uses to tell "this is the same item, just moved" apart from "this is a genuinely new item." Getting it wrong causes real, hard-to-spot state bugs, not just console noise.
4 min read
Rendering a list: the basic shape
function TodoList({ todos }) {
return (
<ul>
{todos.map((todo) => (
<li key={todo.id}>{todo.text}</li>
))}
</ul>
);
}.map() over an array, returning one JSX element per item, is the standard way to render a list in React — nothing special beyond ordinary JavaScript array mapping. key={todo.id} is the one addition React specifically requires (and warns loudly about if it's missing) on every element produced by mapping over an array — the reason for that requirement is the actual subject of this lesson.
What key actually tells React: "this is the same logical item"
When a list re-renders, React needs to figure out which elements are genuinely new, which were removed, and which are the same item just possibly reordered — key is the only signal it has for this. With stable, unique keys (todo.id), React can correctly recognize "the item that was at index 0 is now at index 1, but it's still the same logical item" — and it reuses that element's actual DOM node and any internal state it holds, rather than tearing it down and recreating it. Without a real key (or worse, using the array index as the key, covered next), a reorder looks to React like every item at every changed index turned into a completely different item.
Why using the array index as a key is a real, common bug — not just a lint warning
function TodoList({ todos }) {
return todos.map((todo, index) => (
<li key={index}> {/* looks fine, is genuinely wrong once the list can reorder */}
<input defaultValue={todo.text} />
</li>
));
}If todos is ever reordered, filtered, or has an item removed from the middle, the index associated with each remaining item changes, even though the item itself didn't — React, trusting the index as the key, concludes "the item at index 1 changed" and updates that DOM node's content in place, rather than recognizing the actual item moved. For a plain read-only <li>{todo.text}</li>, this mostly just costs some unnecessary DOM churn. For the <input> above — anything with its own internal state (an uncontrolled input's typed text, a checkbox's checked state, a component's own useState) — using the index as key produces a genuinely visible bug: state that visually appears to "stick" to the wrong row after a reorder, since React reused the DOM node (and its attached state) for what it incorrectly believed was the same logical position, not the same logical item.
The actual rule: a key needs to be stable, unique, and NOT derived from position
// Good — a real, stable identifier that doesn't change when the list reorders
<li key={todo.id}>{todo.text}</li>
// Bad — regenerated fresh every render, defeating the entire purpose of key
<li key={Math.random()}>{todo.text}</li>
// Acceptable ONLY when the list is genuinely static and will never reorder,
// have items inserted in the middle, or be filtered
<li key={index}>{todo.text}</li>A good key comes from the data itself — a database ID, a genuinely unique field already on the item — something that identifies this specific item regardless of where it currently sits in the array. Math.random() as a key is actively worse than using the index: it produces a different key on every single render, guaranteeing React never recognizes any element as "the same" across renders, defeating reconciliation's entire purpose and forcing a full teardown-and-recreate of every list item, every render. The array index is only genuinely safe when the list's order and membership are truly fixed for that render — a real, if narrower, case than it might seem, since most real lists eventually get sorted, filtered, or edited.
key also resets state deliberately — a real, useful pattern, not just a gotcha
function ProfilePage({ userId }) {
return <Comments key={userId} userId={userId} />;
// Changing userId forces React to treat this as a genuinely NEW Comments
// instance — unmounting the old one (discarding its state) and mounting
// a fresh one, rather than reusing the same instance with new props
}The same mechanism that causes the index-as-key bug above can be used deliberately: since key tells React whether something is "the same instance," changing a component's key explicitly is a legitimate, idiomatic way to force React to fully discard and recreate it — resetting all of its internal state — exactly when that's actually the desired behavior, like a Comments component that shouldn't accidentally carry over a draft comment or scroll position from a different user's profile after navigating between profiles without a full page reload.
Further reading
Check your understanding
A quick comprehension check — not tracked, not graded, just for you.
1. What does the `key` prop actually tell React when rendering a list?
2. Why does using the array index as a key cause a real, visible bug specifically for list items with their own internal state (like an uncontrolled input)?
3. Why is `key={Math.random()}` actively WORSE than using the array index as a key?