Skip to content
aviral gupta

// B2.5 · ~30 min · Beginner

Lists and keys

After this lesson you turn arrays of data into lists with map() and filter(), and give every item a key that keeps it apart from the others.

Lesson 5 of 6 in B2 Describing the UI

You will be able to

  • Render an array as JSX with map(), after filter() when only some items belong
  • Give each item a unique, stable key from the data, on the element map returns
  • Explain why index and random keys break lists, and that key is not a prop
  1. Warm-up · Activity 1 of 7

    Warm-up from lesson B2.4: the ternary and &&. A parent renders <Score points={0} />. What text does it show?

    function Score({ points }) {
      return (
        <p>
          {points > 0 ? points + ' points' : 'No points'}
          {points > 99 && ' 🏆'}
        </p>
      );
    }
  2. Predict · Activity 2 of 7

    Predict before you read on. You click the star next to Ada, then click Add Linus, which puts Linus at the top. Which name has the filled star ★ now?

    function Row({ name }) {
      const [starred, setStarred] = useState(false);
      return (
        <li>
          {name}
          <button onClick={() => setStarred(!starred)}>{starred ? '★' : '☆'}</button>
        </li>
      );
    }
    
    function List() {
      const [names, setNames] = useState(['Ada', 'Grace']);
      return (
        <>
          <button onClick={() => setNames(['Linus', ...names])}>Add Linus</button>
          <ul>
            {names.map((name, index) => <Row key={index} name={name} />)}
          </ul>
        </>
      );
    }
  3. Practice · Activity 3 of 7

    Each person has a unique id. Fill in the key, so React can follow each <li> when the order changes.

    function List({ people }) {
      return (
        <ul>
          {people.map(person => <li key={____}>{person.name}</li>)}
        </ul>
      );
    }
    {people.map(person => <li key={}>{person.name}</li>)}
  4. Practice · Activity 4 of 7

    Match each piece of code to what it does.

  5. Practice · Activity 5 of 7

    How many <li> elements does FruitList render?

    const fruits = ['Apple', 'Pear', 'Plum'];
    
    function FruitList() {
      const items = fruits.map(fruit => {
        <li key={fruit}>{fruit}</li>;
      });
      return <ul>{items}</ul>;
    }
  6. Brain teaser · Activity 6 of 7

    Brain teaser. The key is there, but on the <b> inside each <li>. What does React print?

    const team = [
      { id: 1, name: 'Ada' },
      { id: 2, name: 'Grace' }
    ];
    
    function Team() {
      return (
        <ul>
          {team.map(person => (
            <li>
              <b key={person.id}>{person.name}</b>
            </li>
          ))}
        </ul>
      );
    }
  7. Apply · Activity 7 of 7

    Mini-task: a reading list. In main.jsx, keep an array of three books, each with an id, a title, an author and a read flag. Show only the books not read yet: filter, then map each book to a <dt> with the title and a <dd> with the author inside a <dl>. Each book renders two elements, so group them in a Fragment with the book's id as its key (import { Fragment } from 'react'). Run it: no warning may appear.

    Check your work against this list

Build it yourself

Read the worked example, then write the exercises. Your code runs in your browser or on your computer and is never uploaded.

Worked example

A shop list from data

The products live in data.js with an id each, as react.dev recommends. Shop filters the fruit and maps it to <li> elements with key={product.id}. The price list maps every product to two elements, a <dt> and a <dd>, grouped in a Fragment that carries the key. That callback has a block body because it first works out the price, so it needs its own return.

main.jsx

import { Fragment } from 'react';
import { products } from './data.js';

export default function Shop() {
  const fruits = products.filter(product => product.category === 'Fruit');
  const fruitItems = fruits.map(product => <li key={product.id}>{product.name}</li>);

  return (
    <section>
      <h1>Fruit</h1>
      <ul>{fruitItems}</ul>
      <h2>Prices</h2>
      <dl>
        {products.map(product => {
          const price = product.price.toFixed(2) + ' €';
          return (
            <Fragment key={product.id}>
              <dt>{product.name}</dt>
              <dd>{price}</dd>
            </Fragment>
          );
        })}
      </dl>
    </section>
  );
}

data.js

export const products = [
  { id: 'p1', name: 'Apple', category: 'Fruit', price: 0.5 },
  { id: 'p2', name: 'Carrot', category: 'Vegetable', price: 0.3 },
  { id: 'p3', name: 'Banana', category: 'Fruit', price: 0.25 },
  { id: 'p4', name: 'Leek', category: 'Vegetable', price: 1.2 }
];

Run it with

npm run dev

Output

<!-- #root -->
<div id="root"><section><h1>Fruit</h1><ul><li>Apple</li><li>Banana</li></ul><h2>Prices</h2><dl><dt>Apple</dt><dd>0.50 €</dd><dt>Carrot</dt><dd>0.30 €</dd><dt>Banana</dt><dd>0.25 €</dd><dt>Leek</dt><dd>1.20 €</dd></dl></section></div>
  • filter() kept Apple and Banana, in their original order, and map() made one <li> each.
  • The Fragments left no trace: the <dl> holds a flat run of <dt> and <dd>.
  • No key appears in the HTML; React keeps keys for itself.
  • Nothing was printed before the snapshot, so React gave no key warning.
Change it and run it

Tab indents and Shift+Tab outdents. To leave the editor with the keyboard, press Esc, then Tab.

The first run downloads React and the compiler that turns JSX into JavaScript (up to 2 MB) and keeps them cached. Your components run in your browser and stay on your device.

Your page

Your page runs in an isolated frame. It has no access to this site, and its localStorage is kept in memory for this run only: the next run starts empty.

Run the program to see its page here.

Exercises

Exercise 1 of 2

Only the engineers

The team page lists everybody, and React warns about missing keys. Change it to list only the people whose role is Engineer, each as an <li> reading like "Mira Okafor: Engineer", in the order of data.js. Filter first, then map, and give each <li> a key from the data.

Tab indents and Shift+Tab outdents. To leave the editor with the keyboard, press Esc, then Tab.

The first run downloads React and the compiler that turns JSX into JavaScript (up to 2 MB) and keeps them cached. Your components run in your browser and stay on your device.

Your page

Your page runs in an isolated frame. It has no access to this site, and its localStorage is kept in memory for this run only: the next run starts empty.

Run the program to see its page here.

Hints
  1. Hint 1

    people.filter(person => person.role === 'Engineer') gives a new array with the three engineers.

  2. Hint 2

    Map that array instead of people.

  3. Hint 3

    Every person has an id, so the <li> gets key={person.id}.

Show a solution

One way to solve it. Yours can look different and still pass the checks.

import { people } from './data.js';

export default function Engineers() {
  const engineers = people.filter(person => person.role === 'Engineer');
  const listItems = engineers.map(person => (
    <li key={person.id}>
      {person.name}: {person.role}
    </li>
  ));
  return (
    <section>
      <h1>Engineers</h1>
      <ul>{listItems}</ul>
    </section>
  );
}
Run it on your computer

Install React 19.3 or newer. Save these files in one folder, open a terminal in that folder, and run the commands below.

main.jsx

import { people } from './data.js';

// TODO: list only the engineers, each as <li>Name: role</li>,
// with a key that comes from the data.
export default function Engineers() {
  const listItems = people.map(person => <li>{person.name}: {person.role}</li>);
  return (
    <section>
      <h1>Engineers</h1>
      <ul>{listItems}</ul>
    </section>
  );
}

main.test.jsx

import {test} from 'node:test';
import assert from 'node:assert/strict';
import {render, screen} from '@testing-library/react';
import Engineers from './main.jsx';

test('React prints no warnings', () => {
  const warnings = [];
  const original = console.error;
  console.error = (...args) => warnings.push(args.join(' '));
  try {
    render(<Engineers />);
  } finally {
    console.error = original;
  }
  assert.deepEqual(warnings, [], 'console.error while rendering');
});

test('Only the three engineers are listed, in order', () => {
  render(<Engineers />);
  const items = screen.getAllByRole('listitem').map((li) => li.textContent);
  assert.deepEqual(items, ['Mira Okafor: Engineer', 'Lena Wolf: Engineer', 'Ines Duarte: Engineer'], 'the list reads ' + JSON.stringify(items));
});

data.js

export const people = [
  { id: 'u1', name: 'Mira Okafor', role: 'Engineer' },
  { id: 'u2', name: 'Tom Becker', role: 'Designer' },
  { id: 'u3', name: 'Lena Wolf', role: 'Engineer' },
  { id: 'u4', name: 'Sam Park', role: 'Manager' },
  { id: 'u5', name: 'Ines Duarte', role: 'Engineer' }
];

In a Vite React project (npm create vite@latest, template react), put main.jsx in src/ as App.jsx and start it with npm run dev. React renders it inside <StrictMode>, as here.

Run the program:

npm run dev

There is no command for the checks on your computer yet. They are in main.test.jsx, written with node:test and React Testing Library.

Exercise 2 of 2

Stars that stay put

Star Ada, then add a guest at the top: the star jumps to the new guest, because the list uses the index as its key. Fix it: give every new guest an id in handleAdd, where it is created (the counter nextNumber is there for that), and use the id as the key. Keep the button texts; the tests click them.

Tab indents and Shift+Tab outdents. To leave the editor with the keyboard, press Esc, then Tab.

The first run downloads React and the compiler that turns JSX into JavaScript (up to 2 MB) and keeps them cached. Your components run in your browser and stay on your device.

Your page

Your page runs in an isolated frame. It has no access to this site, and its localStorage is kept in memory for this run only: the next run starts empty.

Run the program to see its page here.

Hints
  1. Hint 1

    The starred state belongs to a Guest, and React finds that Guest again by its key. Index 0 is a different guest after an insert at the top.

  2. Hint 2

    In handleAdd, add an id to the new object: { id: 'new' + nextNumber, name: 'Guest ' + nextNumber }.

  3. Hint 3

    In the map, drop index and write <Guest key={guest.id} name={guest.name} />.

Show a solution

One way to solve it. Yours can look different and still pass the checks.

import { useState } from 'react';

function Guest({ name }) {
  const [starred, setStarred] = useState(false);
  return (
    <li>
      {name}{' '}
      <button aria-pressed={starred} onClick={() => setStarred(!starred)}>
        Star {name}
      </button>
    </li>
  );
}

let nextNumber = 1;

export default function GuestList() {
  const [guests, setGuests] = useState([
    { id: 'g1', name: 'Ada' },
    { id: 'g2', name: 'Grace' }
  ]);

  function handleAdd() {
    const guest = { id: 'new' + nextNumber, name: 'Guest ' + nextNumber };
    nextNumber++;
    setGuests([guest, ...guests]);
  }

  return (
    <>
      <button onClick={handleAdd}>Add a guest at the top</button>
      <ul>
        {guests.map(guest => (
          <Guest key={guest.id} name={guest.name} />
        ))}
      </ul>
    </>
  );
}
Run it on your computer

Install React 19.3 or newer. Save these files in one folder, open a terminal in that folder, and run the commands below.

main.jsx

import { useState } from 'react';

function Guest({ name }) {
  const [starred, setStarred] = useState(false);
  return (
    <li>
      {name}{' '}
      <button aria-pressed={starred} onClick={() => setStarred(!starred)}>
        Star {name}
      </button>
    </li>
  );
}

let nextNumber = 1;

export default function GuestList() {
  const [guests, setGuests] = useState([
    { id: 'g1', name: 'Ada' },
    { id: 'g2', name: 'Grace' }
  ]);

  function handleAdd() {
    // TODO: give the new guest an id here, where it is created.
    const guest = { name: 'Guest ' + nextNumber };
    nextNumber++;
    setGuests([guest, ...guests]);
  }

  return (
    <>
      <button onClick={handleAdd}>Add a guest at the top</button>
      <ul>
        {guests.map((guest, index) => (
          <Guest key={index} name={guest.name} />
        ))}
      </ul>
    </>
  );
}

main.test.jsx

import {test} from 'node:test';
import assert from 'node:assert/strict';
import {render, screen} from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import GuestList from './main.jsx';

test('React prints no warnings while two guests are added', async () => {
  const user = userEvent.setup();
  const warnings = [];
  const original = console.error;
  console.error = (...args) => warnings.push(args.join(' '));
  try {
    render(<GuestList />);
    await user.click(screen.getByRole('button', {name: 'Add a guest at the top'}));
    await user.click(screen.getByRole('button', {name: 'Add a guest at the top'}));
  } finally {
    console.error = original;
  }
  assert.deepEqual(warnings, [], 'console.error while adding guests');
});

test('A new guest appears at the top', async () => {
  const user = userEvent.setup();
  render(<GuestList />);
  await user.click(screen.getByRole('button', {name: 'Add a guest at the top'}));
  const first = screen.getAllByRole('listitem')[0].textContent;
  assert.match(first, /^Guest/, 'the first item reads ' + JSON.stringify(first));
});

test('Ada keeps her star when a guest is added at the top', async () => {
  const user = userEvent.setup();
  render(<GuestList />);
  await user.click(screen.getByRole('button', {name: 'Star Ada'}));
  await user.click(screen.getByRole('button', {name: 'Add a guest at the top'}));
  const ada = screen.getByRole('button', {name: 'Star Ada'}).getAttribute('aria-pressed');
  assert.equal(ada, 'true', 'Ada is no longer starred');
  const starred = screen.getAllByRole('button', {pressed: true}).map((button) => button.textContent);
  assert.deepEqual(starred, ['Star Ada'], 'starred buttons: ' + JSON.stringify(starred));
});

In a Vite React project (npm create vite@latest, template react), put main.jsx in src/ as App.jsx and start it with npm run dev. React renders it inside <StrictMode>, as here.

Run the program:

npm run dev

There is no command for the checks on your computer yet. They are in main.test.jsx, written with node:test and React Testing Library.

Common mistakes

Two items with the same id in the data

const fruits = [
  { id: 'f1', name: 'Apple' },
  { id: 'f2', name: 'Banana' },
  { id: 'f2', name: 'Cherry' }
];

export default function FruitList() {
  return (
    <ul>
      {fruits.map(fruit => <li key={fruit.id}>{fruit.name}</li>)}
    </ul>
  );
}

What React prints

Encountered two children with the same key, `f2`.

Why, and the fix

The code is right, but the data is not: Cherry was copied from Banana and kept its id. Keys must be unique among siblings, so React warns, and with duplicates it may duplicate or drop items when the list changes. Fix the data (Cherry gets f3). When you create items yourself, a counter or crypto.randomUUID() makes the id unique.

Reading key as a prop

const fruits = [
  { id: 'f1', name: 'Apple' },
  { id: 'f2', name: 'Banana' }
];

function Fruit({ key, name }) {
  return <li>{key}: {name}</li>;
}

export default function FruitList() {
  return (
    <ul>
      {fruits.map(fruit => <Fruit key={fruit.id} name={fruit.name} />)}
    </ul>
  );
}

What React prints

`key` is not a prop. Trying to access it will result in `undefined` being returned.

Why, and the fix

React keeps the key for itself and does not pass it on, so key is undefined inside Fruit, and the items read ": Apple". The warning starts with the component name, Fruit:. If the component needs the id, pass it as a second prop: <Fruit key={fruit.id} id={fruit.id} name={fruit.name} />.

Using Fragment without importing it

const terms = [
  { id: 't1', word: 'JSX', meaning: 'markup in JavaScript' },
  { id: 't2', word: 'key', meaning: 'an item\'s identity' }
];

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

What React prints

ReferenceError: Fragment is not defined

Why, and the fix

Unlike the short <> syntax, <Fragment> is a component that React exports, so the file must import it. The run prints Uncaught ReferenceError (Chrome, Firefox and Node wording; Safari words it differently). Add import { Fragment } from 'react'; at the top of main.jsx.

React in the browser: React 19.3.0 (development build) and React Testing Library, MIT, served from this site; JSX is compiled by TypeScript 6.0.3, Apache-2.0. Licence and source

Exit ticket

5 questions, no hints. Score 80% or more to complete the lesson.

Finish every activity above to unlock the exit ticket.

Report a problem

Spotted something wrong or unclear? Say what, and it will be checked and fixed.

#

At least 20 characters.

Only if you want a reply.

Key ideas

map() turns data into JSX, filter() picks first

Keep the data in an array and let JavaScript build the list: people.map(person => <li>{person.name}</li>) returns one <li> per item, and {listItems} inside a <ul> shows them all. To show only some items, filter first: people.filter(person => person.role === 'Engineer') returns a new, shorter array, which you then map. An arrow function with an expression after => returns it. With a curly brace after =>, the function has a block body, and you must write return yourself; react.dev warns: if you forget it, nothing gets returned.

A key tells React which item is which

Every element directly inside a map() call needs a key: <li key={person.id}>. The key goes on the outermost element the callback returns, not on something inside it. Keys let React match items between renders when they move, are inserted or are deleted. Take them from the data: database ids, or for data you create yourself, a counter or crypto.randomUUID() at the moment you create the item, never while rendering. Keys must be unique among siblings and must not change. When one item needs several elements, wrap them in <Fragment key={…}>; the short <> syntax cannot take a key.

Not the index, not random, not a prop

Without a key React uses the position. key={index} does the same, and it breaks as soon as items are inserted, deleted or reordered: state such as a star or a typed value stays at its position and ends up next to another item. key={Math.random()} is worse: the keys never match, so React recreates every item on each render and typed input is lost. The key is only a hint for React: your component does not receive it, props.key is undefined, and React warns. If the component needs the id, pass it as another prop as well: <Profile key={id} userId={id} />.

Sources

Last reviewed October 5, 2026