Lists & Keys

React · Lesson 7 of 29 6 min read Updated

Most screens show lists: products in a shop, messages in a chat, trains between two cities. In React you keep the data in an array and turn each item into JSX with the array map() method. Each item also needs a key, a special prop that helps React keep track of which item is which.

In this lesson you will render lists, filter and sort them safely, choose good keys, and see exactly what goes wrong when you use the array index as a key.

Rendering an array with map()

JSX can render an array of elements, so the usual pattern is: start with an array of data, map() each item to JSX, and put the result inside braces.

React JSXTry it yourself
import { createRoot } from "react-dom/client";

const trains = [
  { id: "JP-101", name: "Pink City Express", departs: "06:10", fare: 1035 },
  { id: "JP-102", name: "Aravalli Rajdhani", departs: "19:55", fare: 1490 },
  { id: "JP-103", name: "Hawa Mahal Mail", departs: "13:40", fare: 425 },
  { id: "JP-104", name: "Amer Fort Superfast", departs: "04:30", fare: 510 },
];

function TrainList({ maxFare }) {
  const affordable = trains
    .filter((t) => t.fare <= maxFare)
    .toSorted((a, b) => a.departs.localeCompare(b.departs));

  if (affordable.length === 0) {
    return <p>No trains under ₹{maxFare}.</p>;
  }

  return (
    <ul>
      {affordable.map((train) => (
        <li key={train.id}>
          {train.departs} {train.name} ({train.id}): ₹{train.fare.toLocaleString("en-IN")}
        </li>
      ))}
    </ul>
  );
}

createRoot(document.getElementById("root")).render(
  <>
    <h2>Trains to Jaipur under ₹1,100</h2>
    <TrainList maxFare={1100} />
    <h2>Under ₹300</h2>
    <TrainList maxFare={300} />
  </>
);

filter() keeps only the trains you can afford and toSorted() returns a new sorted array. Avoid sort() on data that came from props or state, because sort() changes the original array, which breaks the purity rule. toSorted(), toReversed() and toSpliced() (added in ES2023) all return copies instead.

The callback returns one <li> per train, and each one gets key={train.id}.

What keys are for

When a list changes, React compares the new list with the old one. Keys tell React which old item matches which new item. With good keys, React can see "the Rajdhani moved to the top" or "one train was removed" and update only that, keeping each item's state and DOM attached to the right data.

Rules for keys:

  • A key must be unique among siblings in the same list. Two different lists can reuse the same keys.
  • A key must be stable: the same item must get the same key on every render. Database ids, roll numbers and train numbers are perfect.
  • Put the key on the outermost element returned from map(). If you map to a component, the key goes on the component: <TrainRow key={t.id} train={t} />.
  • Never generate keys during render with Math.random() or crypto.randomUUID(). A new key every render makes React destroy and rebuild every item.

key is used by React itself and is not passed to your component as a prop. If the component needs the id, pass it again as a separate prop, for example id={t.id}.

Why the array index is a bad key

If you forget the key, React uses the item's position (index) and warns in development. Using key={index} yourself silences the warning but keeps the problem. Type a note next to each name below, then click "Add at top" in both lists:

React JSXTry it yourself
import { useState } from "react";
import { createRoot } from "react-dom/client";

let nextId = 4;

function Roster({ useIndexKey }) {
  const [students, setStudents] = useState([
    { id: 1, name: "Aarav" },
    { id: 2, name: "Meera" },
    { id: 3, name: "Farhan" },
  ]);

  function addAtTop() {
    setStudents([{ id: nextId++, name: "New student" }, ...students]);
  }

  return (
    <section style={{ marginBottom: 16 }}>
      <h3>{useIndexKey ? "key = index (buggy)" : "key = id (correct)"}</h3>
      <button onClick={addAtTop}>Add at top</button>
      <ul>
        {students.map((s, index) => (
          <li key={useIndexKey ? index : s.id}>
            {s.name} <input placeholder="note" size={12} />
          </li>
        ))}
      </ul>
    </section>
  );
}

createRoot(document.getElementById("root")).render(
  <>
    <Roster useIndexKey={true} />
    <Roster useIndexKey={false} />
  </>
);

With index keys, the notes stay in the first three rows while the names move down, so Aarav's note now sits next to "New student". React thought item 0 was still item 0 and only changed its text. With id keys, React knows a new item was inserted and every note stays with its student.

The index is acceptable only when the list never reorders, never has items inserted or removed in the middle, and items have no state. When in doubt, use an id from your data.

Several elements per item

map() must return one element per item. If each item needs two siblings, such as a term and its definition, wrap them in a Fragment with the key. The short <> syntax cannot take a key, so import Fragment:

React JSX
import { Fragment } from "react";

function Glossary({ terms }) {
  return (
    <dl>
      {terms.map((t) => (
        <Fragment key={t.word}>
          <dt>{t.word}</dt>
          <dd>{t.meaning}</dd>
        </Fragment>
      ))}
    </dl>
  );
}

Advanced: grouping a list

Data often needs grouping before display, such as students by city. Object.groupBy() (ES2024) returns an object of arrays, and you can map over its entries to render one sub-list per group. Each level of mapping needs its own keys:

React JSXTry it yourself
import { createRoot } from "react-dom/client";

const students = [
  { roll: 101, name: "Priya", city: "Jaipur" },
  { roll: 102, name: "Kabir", city: "Pune" },
  { roll: 103, name: "Ananya", city: "Jaipur" },
  { roll: 104, name: "Rohan", city: "Kochi" },
  { roll: 105, name: "Isha", city: "Pune" },
];

function ByCity() {
  const groups = Object.groupBy(students, (s) => s.city);

  return Object.entries(groups)
    .toSorted(([a], [b]) => a.localeCompare(b))
    .map(([city, list]) => (
      <section key={city}>
        <h3>{city} ({list.length})</h3>
        <ul>
          {list.map((s) => (
            <li key={s.roll}>{s.roll}: {s.name}</li>
          ))}
        </ul>
      </section>
    ));
}

createRoot(document.getElementById("root")).render(<ByCity />);

A component may return an array like this, because React can render arrays. Cities are unique, so the city name works as the key for each section, and roll numbers key the students inside.

Common mistakes

  • Forgetting return in a map callback with braces. items.map((i) => { <li>{i}</li> }) returns undefined. Use parentheses (i) => (<li>...</li>) or write return.
  • Key on the wrong element. The key goes on the element directly returned by map(), not on a child inside it.
  • Random keys. key={Math.random()} recreates every item on every render.
  • Sorting in place. props.items.sort() changes the parent's array. Use toSorted() or copy first with [...items].sort().

Key points

  • Render lists by mapping an array of data to JSX inside braces.
  • Every item needs a key that is unique among siblings and stable across renders.
  • Use ids from your data as keys; the index breaks when items are inserted, removed or reordered.
  • key is not passed to your component as a prop.
  • Use filter(), toSorted() and Object.groupBy() to prepare data without mutating it.
  • Use <Fragment key={...}> when one item renders several sibling elements.

Exercise

Create an array of five dishes for a canteen menu, each with an id, name, price and veg (true or false). Render only the vegetarian dishes, sorted from cheapest to most expensive, as a list showing name and price. Below it, show the total price of all vegetarian dishes.

Show solution
React JSX
import { createRoot } from "react-dom/client";

const dishes = [
  { id: 1, name: "Rajma chawal", price: 90, veg: true },
  { id: 2, name: "Chicken biryani", price: 160, veg: false },
  { id: 3, name: "Masala dosa", price: 70, veg: true },
  { id: 4, name: "Egg roll", price: 60, veg: false },
  { id: 5, name: "Paneer tikka", price: 140, veg: true },
];

function VegMenu() {
  const veg = dishes.filter((d) => d.veg).toSorted((a, b) => a.price - b.price);
  const total = veg.reduce((sum, d) => sum + d.price, 0);

  return (
    <>
      <ul>
        {veg.map((d) => (
          <li key={d.id}>{d.name}: ₹{d.price}</li>
        ))}
      </ul>
      <p>Total: ₹{total}</p>
    </>
  );
}

createRoot(document.getElementById("root")).render(<VegMenu />);

filter and toSorted produce a new array, the dish id is a stable key, and the total is calculated from the same filtered array.

Quick Quiz

1. Which is the best key for a list of orders from a database?

2. Where does the key go when you map to a component?

3. Why can index keys cause bugs?

4. Which method sorts without changing the original array?

Learn React with a mentor

Free lessons get you started. Our Full Stack Web Development training adds real projects and code reviews from working developers.

  • Project-based learning
  • Mentor code reviews
  • Free counselling call