DevgainsDevgainsDevgains
All articles

Keys in Lists: The Subtle React Bug You Ship Every Time You Get Them Wrong

·4 min read
Keys in Lists: The Subtle React Bug You Ship Every Time You Get Them Wrong

Photo: Unsplash

The key prop looks like a formality. You map over an array, React nags you in the console, you slap key={index} on it, the warning disappears, and you move on. Months later a user reports that deleting the second item in a list clears the wrong row's text — and you've just met the most common avoidable bug in React.

Keys aren't decoration. They're how React decides which component instance maps to which piece of data across renders. Get them wrong and React keeps the wrong DOM and the wrong state.

What keys actually do

When a list re-renders, React has to match the new array of elements against the previous one to decide what to create, update, or destroy. Keys are the identity it matches on. The React docs on rendering lists put it plainly: a key tells React which array item each component corresponds to, even if items move.

Without stable keys, React falls back to position. Item 0 is matched to item 0, item 1 to item 1, and so on. That's fine until the list reorders, and then position lies.

Why key={index} breaks

Imagine a list of three to-dos, each rendered with a controlled input holding draft text. You key by index. Now you delete the first to-do:

Before delete:           After delete (key = index):
  key 0 → "buy milk"        key 0 → was "call bank"   ← DOM/state for index 0 stays
  key 1 → "call bank"       key 1 → was "ship order"
  key 2 → "ship order"      (key 2 unmounted)

React sees keys 0 and 1 still present, so it keeps those component instances and just updates their props. But the data shifted up by one. The input that held "buy milk"'s draft is now showing "call bank"'s row — with the old DOM state still attached. Local state, focus, scroll position, and animation state all stick to the position, not the item.

Index keys are safe only for lists that are static — never reordered, inserted into, or filtered. The moment items can move, index keys attach state to the wrong row.

Use a stable identity

The fix is to key by something intrinsic to the item that survives reordering — a database id, a UUID, a slug. Not the index, and not something you generate during render.

{todos.map((todo) => (
  <TodoRow key={todo.id} todo={todo} />   // identity travels with the data
))}

If your data genuinely has no id, generate one when you create the item, not in render:

// Good: id is created once, when the item is added.
setTodos((prev) => [...prev, { id: crypto.randomUUID(), text: "" }]);

Generating key={crypto.randomUUID()} inside map is the opposite bug: a brand-new key every render means React unmounts and remounts every row every time, destroying state and killing performance.

Keys are local, and they're not props

Two things people miss:

  • Keys only need to be unique among siblings, not globally. The same key in two different lists is fine.
  • key is not readable as a prop. If a child needs the value, pass it again explicitly:
<TodoRow key={todo.id} id={todo.id} todo={todo} />

A deliberate trick: change the key to reset state

Once you understand that a changed key means "this is a different instance," you can use it on purpose. Changing a component's key forces React to discard its state and remount — the cleanest way to reset a form when the underlying entity changes:

// Switching profiles resets all internal form state, no useEffect needed.
<ProfileForm key={selectedUserId} userId={selectedUserId} />

The React docs describe this as resetting state with a key, and it's far more reliable than trying to clear fields manually.

Takeaways

  • Keys give list items a stable identity so React maps data to the right component instance across renders.
  • key={index} attaches state and DOM to a position, so it breaks on reorder, insert, or filter.
  • Key by an intrinsic, stable id; generate ids when items are created, never during render.
  • Keys must be unique among siblings only, and key is not readable as a prop — pass the value again if needed.
  • Deliberately changing a key is the cleanest way to reset a component's state.
4 min read

Read next