Most interfaces show lists: products, messages, search results, menu items. You rarely write those by hand. Instead, the data lives in an array, and React turns each item into a piece of JSX. This lesson on React lists and keys shows how to do that with the array methods you met in JavaScript Arrays, and why React asks you to give every item a key.
React lists and keys: rendering an array with map()
map() creates a new array by running a function on every item. If that function returns JSX, you get an array of elements, and React can render an array placed inside curly braces. That is all list rendering is.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>A list from an array of objects</title>
<script src="https://unpkg.com/@babel/[email protected]/babel.min.js"></script>
<style>
body { font-family: system-ui, sans-serif; padding: 16px; line-height: 1.5; }
li { margin-bottom: 4px; }
.km { color: #666; font-size: 14px; }
</style>
</head>
<body>
<div id="root"></div>
<script type="text/babel" data-type="module">
import React from "https://esm.sh/[email protected]";
import { createRoot } from "https://esm.sh/[email protected]/client";
const trips = [
{ id: "t1", place: "Auroville", km: 12 },
{ id: "t2", place: "Mahabalipuram", km: 95 },
{ id: "t3", place: "Gingee Fort", km: 68 },
{ id: "t4", place: "Chidambaram", km: 66 }
];
function App() {
return (
<div>
<h2>Day trips from Pondicherry</h2>
<ul>
{trips.map(trip => (
<li key={trip.id}>
{trip.place} <span className="km">({trip.km} km)</span>
</li>
))}
</ul>
</div>
);
}
createRoot(document.getElementById("root")).render(<App />);
</script>
</body>
</html>You can also build the array first and place it later: const items = trips.map(…); and then <ul>{items}</ul>. Both styles are fine; keeping map() inline is more common for short lists.
Filtering before rendering
To show only some items, chain filter() before map(). Combined with state, this gives you a live filter. Note that the filtered list is calculated during rendering; it is not stored in a second piece of state. That keeps the original array as the single source of truth.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>A filterable list</title>
<script src="https://unpkg.com/@babel/[email protected]/babel.min.js"></script>
<style>
body { font-family: system-ui, sans-serif; padding: 16px; line-height: 1.5; }
label { margin-right: 12px; }
li { margin-bottom: 4px; }
</style>
</head>
<body>
<div id="root"></div>
<script type="text/babel" data-type="module">
import React, { useState } from "https://esm.sh/[email protected]";
import { createRoot } from "https://esm.sh/[email protected]/client";
const dishes = [
{ id: 1, name: "Idiyappam", veg: true },
{ id: 2, name: "Fish curry", veg: false },
{ id: 3, name: "Avial", veg: true },
{ id: 4, name: "Mutton biryani", veg: false },
{ id: 5, name: "Bisi bele bath", veg: true }
];
function App() {
const [vegOnly, setVegOnly] = useState(false);
const [search, setSearch] = useState("");
const visible = dishes
.filter(d => !vegOnly || d.veg)
.filter(d => d.name.toLowerCase().includes(search.toLowerCase()));
return (
<div>
<label>
<input type="checkbox" checked={vegOnly} onChange={e => setVegOnly(e.target.checked)} /> Veg only
</label>
<label>
Search <input value={search} onChange={e => setSearch(e.target.value)} />
</label>
<p>{visible.length} of {dishes.length} dishes</p>
<ul>
{visible.map(d => <li key={d.id}>{d.name}{d.veg ? " (veg)" : ""}</li>)}
</ul>
{visible.length === 0 && <p>No dishes match.</p>}
</div>
);
}
createRoot(document.getElementById("root")).render(<App />);
</script>
</body>
</html>Sorting without mutating
Be careful with sort() and reverse(): they change the original array in place, which breaks the rule that state and props must not be mutated. Copy the array first with [...items].sort(…), or use the newer toSorted() and toReversed() methods, which return a new array and leave the original alone. Like filtering, sorting is usually done during rendering from the original data.
Why React needs keys
A key is a special attribute that gives each list item a stable identity. When the list changes – an item is added, removed or moved – React compares the old and new keys to work out which item is which. With good keys, React keeps each item’s DOM and state attached to the right data, and updates only what changed. Think of keys as file names in a folder: you can sort the folder any way you like and still know which file is which.
If you forget the key, React shows a warning in the console: “Each child in a list should have a unique key prop”.
Rules for keys
- Unique among siblings. Two items in the same list cannot share a key. Items in different lists can.
- Stable. The same item must get the same key on every render. Never use
Math.random()or a new id generated during rendering. - From the data. Use ids from your database or API. For items created in the browser, create the id once when the item is added, for example with
crypto.randomUUID()or a counter. - On the outer element returned by
map()– the<li>or the component, not something inside it.
The trouble with index keys
The second argument of map() is the index, and it is tempting to write key={index}. It removes the warning, and it is acceptable for a list that never changes order and never has items removed. But if you delete or reorder items, the indexes shift, and React matches state to the wrong item. In the demo below, type something in the note box of the first person, then remove that person. With index keys, the note jumps to the next person.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Index keys versus id keys</title>
<script src="https://unpkg.com/@babel/[email protected]/babel.min.js"></script>
<style>
body { font-family: system-ui, sans-serif; padding: 16px; line-height: 1.5; }
.cols { display: flex; gap: 24px; flex-wrap: wrap; }
.row { margin: 4px 0; }
input { width: 110px; }
</style>
</head>
<body>
<div id="root"></div>
<script type="text/babel" data-type="module">
import React, { useState } from "https://esm.sh/[email protected]";
import { createRoot } from "https://esm.sh/[email protected]/client";
const start = [
{ id: "a", name: "Lakshmi" },
{ id: "b", name: "Farhan" },
{ id: "c", name: "Joseph" }
];
function Row({ name, onRemove }) {
return (
<div className="row">
{name} <input placeholder="note" aria-label={"Note for " + name} />{" "}
<button type="button" onClick={onRemove}>Remove</button>
</div>
);
}
function PeopleList({ useIndex }) {
const [people, setPeople] = useState(start);
function remove(id) {
setPeople(people.filter(p => p.id !== id));
}
return (
<div>
<h3>{useIndex ? "key={index} (buggy)" : "key={person.id} (correct)"}</h3>
{people.map((p, index) => (
<Row key={useIndex ? index : p.id} name={p.name} onRemove={() => remove(p.id)} />
))}
<button type="button" onClick={() => setPeople(start)}>Reset</button>
</div>
);
}
function App() {
return (
<div className="cols">
<PeopleList useIndex={true} />
<PeopleList useIndex={false} />
</div>
);
}
createRoot(document.getElementById("root")).render(<App />);
</script>
</body>
</html>Tip: Removing an item is done with filter(), which returns a new array – never with splice(), which changes the array in place. This follows the state rules from React State with useState.
Keys on fragments
Sometimes each item needs to produce several elements without a wrapper, such as a <dt> and <dd> pair. The short <></> syntax cannot take a key, so import Fragment and write <Fragment key={item.id}>…</Fragment>. Also remember that your component does not receive key as a prop; if it needs the id, pass it again, e.g. <Row key={p.id} id={p.id} />.
import { Fragment } from "react";
function Glossary({ terms }) {
return (
<dl>
{terms.map(t => (
<Fragment key={t.id}>
<dt>{t.word}</dt>
<dd>{t.meaning}</dd>
</Fragment>
))}
</dl>
);
}