Skip to content

Lists and keys

Say you're building a to-do list: a handful of items, each with its own text, pulled from an array of todo objects. React doesn't have a special list component for turning that array into markup. You use plain JavaScript's map to turn each item into a piece of JSX, and React renders the results.

jsx
function TodoList({ todos }) {
  return (
    <ul>
      {todos.map(todo => (
        <li key={todo.id}>{todo.text}</li>
      ))}
    </ul>
  )
}

Rendering a list with map

That todos array usually arrives as a prop passed down from a parent component that owns the actual data. todos.map walks the array and returns one <li> for each todo. The curly braces around it drop that array of JSX elements straight into the <ul>, the same way you'd drop in a single expression. Each <li> also gets a key prop, set to todo.id. That part isn't optional if you want the list to behave correctly: skip it, and React still renders the list, but it logs a console warning, and the reordering bugs described below become real risks the first time the list changes shape.

A key is how React tells list items apart from one render to the next. It has to be unique among its siblings, so no two <li> elements in this list share a key, and it has to be stable, meaning the same todo gets the same key every time the list re-renders. An id from your data, like todo.id, is exactly that: it belongs to the todo, not to its position in the array, so it stays put even if the list around it changes.

Why the array index makes a poor key

That last point is why the array index makes a poor key. It's tempting, since every array already has one:

jsx
{todos.map((todo, index) => (
  <li key={index}>{todo.text}</li>
))}

This works fine as long as the list never reorders, and never has items inserted or deleted. The moment it does, the index stops matching the item it used to point to. Delete the first todo and every remaining item shifts up one index, so React sees the same keys attached to different todos. If any of those list items hold their own state, like a checkbox mid-edit or an input someone's typing into, that state stays attached to the index and ends up on the wrong row. Treat the index as a fallback for lists that are static and never reorder. Reach for a real id everywhere else.

A good key comes from the data, not from the render. If your todos come from an API or a database, they almost certainly already carry an id: use it directly instead of deriving something new. A key only needs to be unique among the siblings produced by that one map call, not across your whole app, so a todo list and a completed-items list built from the same data can both key off todo.id with no clash. React only compares keys within a single set of children at a time.

The key also has to sit on the element map returns directly, not on something nested inside it:

jsx
// wrong: the key is on an inner element, so React never sees it
{todos.map(todo => (
  <li>
    <span key={todo.id}>{todo.text}</span>
  </li>
))}

// right: the key is on the outermost element the callback returns
{todos.map(todo => (
  <li key={todo.id}>{todo.text}</li>
))}

React reads keys off the top-level element of each item in the array. A key buried inside a child element doesn't count, and you'll still get the "each child in a list should have a unique key" warning even though a key exists somewhere in the JSX.

The index isn't always wrong, either. For a list that renders once and never reorders, filters, or splices, a footer's set of static links, for example, key={index} holds up fine because the index and the item it points to never drift apart. The problem starts the moment that list can change shape: reordering rows, filtering a search result as someone types, deleting an item. At that point the index starts pointing at different data than it did last render, and that's when state and DOM nodes get attached to the wrong row.

Sometimes there's no id in the data at all, say a plain array of strings, or an array built from a computation with no natural identifier. In that case, build a composite key from whatever combination of fields is actually stable and unique for that list, such as ${todo.category}-${todo.text}, rather than defaulting to the index.

Keys are what make React's reconciliation work correctly on a list. When a component re-renders, React compares the new list of elements to the old one to figure out the minimal set of DOM changes needed, and it uses the key to match elements across that comparison. Same key in both renders means React treats it as the same element: it updates it in place and keeps its DOM node, its internal state, and anything else attached to it. No matching key in the old render means React treats it as new and mounts it fresh. A key that disappears between renders means React unmounts that element and throws its state away.

This is what actually breaks when the key is wrong. Say a list item hidden behind an index key holds its own state, an "expanded" flag on an accordion row, for example. Reorder the underlying array without changing the keys, and React still matches old index 2 to new index 2. It sees "the same element," reuses that DOM node and its state, and hands the expanded flag to whatever todo now happens to sit at index 2. Nothing throws, nothing warns you in the console. Rows quietly show the wrong state, and whoever's debugging it rarely suspects a key problem at first. A stable id sidesteps this because it moves with the data, not with the array position, so reconciliation matches the right element to the right item no matter how the list gets reordered.

JunoKeys tell React which item is which When you turn an array into a list of elements with map, give each one a key, and use a real id from your data, not its position in the array. The key is how React keeps track of which element is which between renders, and an id that belongs to the item stays correct even if the list gets reordered.
JunoKeys tell React which item is which Rendering a list is array.map returning JSX, with a key on the outermost element of each item. Reach for a stable id from your data. The array index looks like a shortcut, but it breaks as soon as the list can reorder, insert, or delete, because the index no longer lines up with the same underlying item, and state can end up attached to the wrong row.
JunoKeys tell React which item is which Keys are the identity React uses for reconciling a list across renders: same key, same element, state and DOM node carried over; missing key, unmount. An index key is only safe for a list that never changes. Anything that reorders, inserts, or deletes needs a real, stable id, or you'll get state silently attached to the wrong row with no error to point you at it.

Next up: State, where a component starts remembering things between renders.