React & Next.js

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.

Intermediate

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?