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.
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
Hint 1
people.filter(person => person.role === 'Engineer') gives a new array with the three engineers.
Hint 2
Map that array instead of people.
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.
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));
});
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
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.
Hint 2
In handleAdd, add an id to the new object: { id: 'new' + nextNumber, name: 'Guest ' + nextNumber }.
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.
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.
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.
`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} />.
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.
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} />.