Lists & Keys
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.
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()orcrypto.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:
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:
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:
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
returnin a map callback with braces.items.map((i) => { <li>{i}</li> })returnsundefined. Use parentheses(i) => (<li>...</li>)or writereturn. - 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. UsetoSorted()or copy first with[...items].sort().
Key points
- Render lists by mapping an array of data to JSX inside braces.
- Every item needs a
keythat 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.
keyis not passed to your component as a prop.- Use
filter(),toSorted()andObject.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
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?
A database id is unique and stays the same for the same order on every render.
2. Where does the key go when you map to a component?
The key belongs on the outermost element returned by map().
3. Why can index keys cause bugs?
React matches items by key, so a shifting index makes it reuse the wrong item's state and DOM.
4. Which method sorts without changing the original array?
toSorted() returns a new sorted array, which keeps props and state unchanged.
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
