Himanshu SharmaSoftware engineer
Let’s talk

React · Lesson 11 of 38

Rendering lists and keys

Turn arrays into lists of elements with map, filter and sort, give every item a stable key, and understand why array indexes make poor keys.

16 minComponents and propsUpdated 24 October 2026

By the end of this lessonRender a list from an array of data with map, choose correct keys, and filter or sort the list without changing the original array.

Almost every app shows a list: products, messages, tasks, search results. In React a list is not a special feature. You turn an array of data into an array of elements with map, and React renders the array. The one extra rule is that each item needs a key.

map turns data into elements

jsx
function App() {
  const fruits = ["Apple", "Banana", "Cherry"];

  return (
    <ul>
      {fruits.map(fruit => (
        <li key={fruit}>{fruit}</li>
      ))}
    </ul>
  );
}

export default App;
result
Apple
Banana
Cherry

fruits.map(...) calls the arrow function once per item and collects the returned <li> elements in a new array. React renders an array of elements one after another. Note the round brackets around the JSX (so that the arrow function returns it) and the key prop on the li.

Lists of objects, with a component per item

Real data is a list of objects. A good pattern is to make a component for one item, and to map over the data to create them:

jsx
const products = [
  { id: 101, name: "Notebook", price: 4.5, inStock: true },
  { id: 102, name: "Backpack", price: 39.99, inStock: false },
  { id: 103, name: "Pencil case", price: 6, inStock: true },
];

function ProductRow({ name, price, inStock }) {
  return (
    <tr>
      <td>{name}</td>
      <td>${price.toFixed(2)}</td>
      <td>{inStock ? "Yes" : "No"}</td>
    </tr>
  );
}

function App() {
  return (
    <table>
      <thead>
        <tr><th>Product</th><th>Price</th><th>In stock</th></tr>
      </thead>
      <tbody>
        {products.map(product => (
          <ProductRow key={product.id} {...product} />
        ))}
      </tbody>
    </table>
  );
}

export default App;
result
Product	Price	In stock
Notebook	$4.50	Yes
Backpack	$39.99	No
Pencil case	$6.00	Yes

The key goes on the element that map returns (here <ProductRow>), not inside it. {...product} spreads the object's properties as props (including id, which the component simply ignores).

What keys are for

A key is a label React uses to tell the items of a list apart. When the data changes (an item is added, removed or moved), React compares the old list with the new one by key. Items with the same key are the same item, so React keeps their DOM nodes and their state, and updates only what changed. Without keys, React cannot tell, so it falls back to position and may do needless work or mix things up.

If you forget the key, React shows a warning in the console:

jsx
function App() {
  const names = ["Asha", "Ravi"];
  return (
    <ul>
      {names.map(name => <li>{name}</li>)}
    </ul>
  );
}

export default App;
result
Asha
Ravi
CONSOLE
error: Each child in a list should have a unique "key" prop.
Check the render method of `App`. See https://react.dev/link/warning-keys for more information.

The list still renders, but the Console shows that each child in a list should have a unique key prop. Treat that warning as an error.

The rules for keys

  • Keys must be unique among siblings (not across the whole app).
  • Keys must be stable: the same item must have the same key on every render. Never generate them during render (key={Math.random()}), which makes React rebuild every item each time.
  • Keys must be strings or numbers.
  • key is not available inside the component as a prop. If the component needs the id, pass it under another name too (id={product.id}).
  • Put the key on the outermost element of the item. Inside a fragment, write <Fragment key={id}> imported from React, because the short <> syntax cannot take a key.

Where keys come from

The right key is the item's identity: a database id, a unique slug, an email address. Data from a server almost always has an id. If you create the data yourself, assign an id when you create the item (for example with crypto.randomUUID()), and store it with the item.

Why not the array index?

It is tempting to write key={index} because it is always available, and it silences the warning. It is correct only when the list is static: never reordered, filtered, inserted into or deleted from. Otherwise it causes bugs. Here is the proof. Each row remembers whether it has been liked, using a piece of state (useState, explained in the State lessons: for now it just remembers a value for that row). In the test, I liked Ravi (the second row), then removed Asha (the first row):

jsx
import { useState } from "react";

function Row({ name, onRemove }) {
  const [liked, setLiked] = useState(false);
  return (
    <li>
      {name}{" "}
      <button className="like" onClick={() => setLiked(!liked)}>{liked ? "♥" : "♡"}</button>{" "}
      <button className="remove" onClick={onRemove}>Remove</button>
    </li>
  );
}

function App() {
  const [people, setPeople] = useState([
    { id: 1, name: "Asha" },
    { id: 2, name: "Ravi" },
  ]);

  return (
    <ul>
      {people.map((person, index) => (
        <Row
          key={index}
          name={person.name}
          onRemove={() => setPeople(people.filter(p => p.id !== person.id))}
        />
      ))}
    </ul>
  );
}

export default App;
result
Ravi ♡ Remove

Ravi was liked (♥), but after Asha was removed Ravi shows an empty heart. With index keys, Ravi moved to position 0 and so got key 0, which React matched with the old row at position 0, Asha's row and its state; Ravi's old row (key 1) was thrown away. The text was updated, but the remembered "liked" belonged to the wrong person. Now use the item's identity as the key and repeat the same test:

jsx
import { useState } from "react";

function Row({ name, onRemove }) {
  const [liked, setLiked] = useState(false);
  return (
    <li>
      {name}{" "}
      <button className="like" onClick={() => setLiked(!liked)}>{liked ? "♥" : "♡"}</button>{" "}
      <button className="remove" onClick={onRemove}>Remove</button>
    </li>
  );
}

function App() {
  const [people, setPeople] = useState([
    { id: 1, name: "Asha" },
    { id: 2, name: "Ravi" },
  ]);

  return (
    <ul>
      {people.map(person => (
        <Row
          key={person.id}
          name={person.name}
          onRemove={() => setPeople(people.filter(p => p.id !== person.id))}
        />
      ))}
    </ul>
  );
}

export default App;
result
Ravi ♥ Remove

Now Ravi keeps the filled heart. Keys are identity, and an index is only a position.

Filtering and sorting lists

Because map works on arrays, you can prepare the data first. Use filter to show a subset and sort to change the order, but remember that sort changes the array it is called on. In React you must not change data in place, so sort a copy:

jsx
const products = [
  { id: 1, name: "Notebook", price: 4.5 },
  { id: 2, name: "Backpack", price: 39.99 },
  { id: 3, name: "Pencil case", price: 6 },
  { id: 4, name: "Calculator", price: 18 },
];

function App() {
  const affordable = products.filter(p => p.price < 20);
  const byPriceDesc = [...affordable].sort((a, b) => b.price - a.price);   // copy, then sort

  return (
    <div>
      <p>Under 20, most expensive first:</p>
      <ol>
        {byPriceDesc.map(p => (
          <li key={p.id}>{p.name} (${p.price})</li>
        ))}
      </ol>
      <p>Original order is unchanged: {products.map(p => p.id).join(", ")}</p>
    </div>
  );
}

export default App;
result
Under 20, most expensive first:
Calculator ($18)
Pencil case ($6)
Notebook ($4.5)
Original order is unchanged: 1, 2, 3, 4

Newer JavaScript has toSorted() and toReversed(), which return copies and do the same job without the spread. Use them if your target browsers support them (all current ones do).

Nested lists and empty lists

A list inside a list is a map inside a map. Give each level its own keys. And always decide what an empty list should show:

jsx
const groups = [
  { id: "a", title: "Fruits", items: ["Apple", "Banana"] },
  { id: "b", title: "Vegetables", items: [] },
];

function App() {
  return (
    <div>
      {groups.map(group => (
        <section key={group.id}>
          <h3>{group.title}</h3>
          {group.items.length === 0 ? (
            <p><em>Nothing here yet.</em></p>
          ) : (
            <ul>
              {group.items.map(item => <li key={item}>{item}</li>)}
            </ul>
          )}
        </section>
      ))}
    </div>
  );
}

export default App;
result
Fruits
Apple
Banana
Vegetables
Nothing here yet.

Inner keys only need to be unique within their own list, which is why both lists could contain an item of the same name.

Performance note

Rendering a few hundred items is fast. For thousands of rows (a big table or feed), the cost is in the DOM, not React. Use pagination or virtualisation (render only the rows in view, with a library such as TanStack Virtual) before you reach for tricks.

Common mistakes

  • Using forEach instead of map; it returns nothing, so no elements are rendered.
  • Forgetting the key, or putting it on an element inside the component instead of on the one map returns.
  • Using index or Math.random() as the key for lists that can change.
  • Sorting the original array with .sort() or .reverse() inside the component.
  • Using a non-unique key such as a name that can repeat.
  • Writing {items.map(...)} with curly braces in the arrow function body, but no return: items.map(i => { <li>...</li> }) returns nothing. Use round brackets or add return.

Exercise

  1. Render a list of your five favourite movies from an array of objects with id, title and year.
  2. Show only movies from the last 20 years, sorted by year, without changing the original array.
  3. Add a message "No movies match" when the filtered list is empty.
  4. In the index-key example, switch to key={person.id} and repeat the removal test to see the difference.

Key points

  • Use array.map to turn data into elements; filter and sort (on a copy) to show subsets and orders.
  • Every element in a list needs a key prop that is unique among its siblings and stays the same for the same piece of data.
  • Use an id from your data as the key; array indexes are only safe for lists that never reorder, insert or delete.
  • Keys are not passed to the component as props; React uses them to match old and new items between renders.

Check yourself

1. Which method turns an array of data into an array of elements?
2. What is the best key for items from a database?
3. What problem can happen with array indexes as keys?
4. How do you show the list sorted by name without changing the original array?