React from the ground up

Lists and keys

Turning an array into elements, the key the transform asks for by name, and why the item's position is the one key you should not use.

Part 7 of 12

Beginner14 min readBack to Courses

From array to elements

Rendering a list is a map: items.map((item) => <li>{item}</li>) turns data into an array of elements, and JSX will render an array happily. The filtering and sorting happen in the same expression or on a line above — items.filter(...).map(...) — because it is all ordinary JavaScript before it is JSX.

The result is one expression, so it can sit inside braces in the middle of markup. A list of names, a table of rows, a row of buttons: same shape, different element.

The key, and what React does with it

Each element in the array needs a key — a string that identifies the item among its siblings. React uses keys to match what was on screen with what is now described: same key means same item, moved or updated; a new key means a new item, built from scratch.

The key must be stable and unique among siblings. Real data usually has one: an id, a slug, an email. The array index is the trap — it is stable only as long as the list is. Insert an item at the top of an index-keyed list and every key shifts by one: React matches the wrong state to the wrong row, and a text input or checkbox quietly attaches itself to the wrong item. The console says so, in the development build this preview runs.

Keys are for React, not for the page

A key is not rendered and not passed to the component as a prop: <Row key={row.id} row={row} /> — the component sees row, never key. If the data inside needs the identifier too, pass it as its own prop.

And a key only matters among siblings. Two lists on a page may both start at key "1" without a word of complaint; the scope is the array, not the document.

An empty list needs no special case

items.map(...) on an empty array gives an empty array, and JSX renders it as nothing at all. That is why a list needs no branch for emptiness: the map is the same code whether there are three items or none, and the empty message is a separate expression beside it.

What the key is matched against

React matches keys within one array, against the keys it saw in the same place last time. It does not compare keys across different lists, and it does not use them to find data — the data is already in the array. The key is only the answer to "is this the same row as before".

Filter, map, key

const { useState } = React;

const books = [
  { id: 'b1', title: 'The Go Programming Language', year: 2015 },
  { id: 'b2', title: 'Learning React', year: 2017 },
  { id: 'b3', title: "You Don't Know JS", year: 2015 },
];

function App() {
  const [query, setQuery] = useState('');

  const shown = books
    .filter((book) => book.title.toLowerCase().includes(query.toLowerCase()))
    .sort((a, b) => a.title.localeCompare(b.title));

  return (
    <div>
      <input
        value={query}
        onChange={(e) => setQuery(e.target.value)}
        placeholder="Search titles"
      />

      <ul>
        {shown.map((book) => (
          <li key={book.id}>
            {book.title} ({book.year})
          </li>
        ))}
      </ul>

      <p>
        {shown.length} of {books.length} shown
      </p>
    </div>
  );
}

What a key lets React do

React keeps a record of the elements it drew last time. When a new array arrives, it matches each element by key and type: the same key means the same item, so the DOM node and any state inside it can be reused; a new key means a new item, built from scratch.

Without a key React falls back to position, which is exactly the index. That is why the warning exists and why the bug is subtle: the list still looks right, but the state inside each row now belongs to whichever item moved into that slot.

Keys and identity

A key has to come from the data, not from the loop. An id, a slug, an email — something that stays with the item when the list is filtered, sorted or reordered. The key has to be unique among siblings, and it only needs to be unique there.

Keys are not props. React consumes the key before it calls your component, so key={item.id} never arrives as props.key; pass the id separately if the component needs it. And a key is not a label: it never appears on the page and has no effect on styling.

Three list mistakes: using the array index as the key on a list that can change; putting the key on the wrong element, so the outermost element in the map has none; and sorting the array in place with sort before mapping, which mutates state and can leave the original order broken. Copy first with [...items].sort(...).

Try these next

  • Add a search field that filters the packing list by name.
  • Show the number of packed items by filtering the array and reading its length.
  • Remove an item in the middle and watch which row keeps its checkbox after you switch to index keys.

A filterable reading list

Run it and search for a title. Then delete `key={book.id}` and watch the console; put it back, and add a button per row that removes that book — the removal needs the id, and the key is not a prop, so pass it.

Live Preview
Console output will appear here...

A packing list with row state

Run it, tick a few items, then remove the first one. The ticks stay with the right items because each row is keyed by id. Now change the key to the index and repeat — the ticks jump.

Live Preview
Console output will appear here...

A missing key is not a crash — it is a warning in the console and a list React cannot keep straight. The warning names the element: "Each child in a list should have a unique key prop".

Check what you learned

One question at a time. A wrong answer explains itself — take another go.

Part 1 of 5

What is the key for?