Rendering Lists & Keys
Most real UIs are driven by data—a list of products, comments, or todos—so you rarely write elements out by hand. Instead you transform an array into an array of JSX elements, almost always with Array.prototype.map(). The moment you render a list, React asks for a key on each item, and getting that key right is the difference between a fast, correct UI and subtle bugs around state, focus, and animation. This page covers how to render lists and why stable keys matter so much.
Rendering an array with map
JSX can render an array of elements directly. The idiomatic pattern is to call .map() on your data and return one element per item. Because JSX expressions live in curly braces, you embed the whole .map() call right where the elements should appear.
function ProductList({ products }) {
return (
<ul>
{products.map((product) => (
<li key={product.id}>
{product.name} — ${product.price}
</li>
))}
</ul>
);
}
const products = [
{ id: "p1", name: "Keyboard", price: 79 },
{ id: "p2", name: "Mouse", price: 29 },
{ id: "p3", name: "Monitor", price: 199 },
];
Rendering <ProductList products={products} /> produces:
Output:
• Keyboard — $79
• Mouse — $29
• Monitor — $199
Note the arrow function returns the <li> element with implicit return parentheses. If you need logic before returning, use a block body with an explicit return.
Every list item needs a key
When you render a list, React requires a key prop on each top-level element produced by the loop. A key is a string or number that uniquely identifies that item among its siblings. Omit it and you get the familiar warning:
Output:
Warning: Each child in a list should have a unique "key" prop.
The key belongs on the outermost element returned by the iteration—not on a child inside it. If you map to a Fragment, use the explicit <Fragment key={...}> form so the key has somewhere to live.
import { Fragment } from "react";
function Glossary({ entries }) {
return (
<dl>
{entries.map((entry) => (
<Fragment key={entry.term}>
<dt>{entry.term}</dt>
<dd>{entry.definition}</dd>
</Fragment>
))}
</dl>
);
}
What keys actually do
When state changes and React re-renders a list, it must reconcile the new array of elements against the previous one to decide what to keep, move, update, or remove. Keys are the identity tags React uses for this matching. With stable keys, React pairs each old element to its new counterpart by key, preserving that element’s DOM node and component state even if the item moved position. Without keys, React falls back to matching by index, assuming position equals identity—which is wrong as soon as the list reorders.
A key only needs to be unique among siblings in the same list. It does not need to be globally unique, and React never exposes the key as a prop to your component—reading
props.keyreturnsundefined.
The index-as-key trap
It is tempting to write key={index}, and it even silences the warning. But the index is just the item’s current position, so when items are inserted, deleted, or reordered, the same index now points at a different item. React then reuses the wrong DOM node and stale internal state.
import { useState } from "react";
function TodoList() {
const [todos, setTodos] = useState([
{ id: 1, text: "Write tests" },
{ id: 2, text: "Ship feature" },
]);
const addToTop = () =>
setTodos((prev) => [{ id: Date.now(), text: "Urgent task" }, ...prev]);
return (
<>
<button onClick={addToTop}>Add to top</button>
<ul>
{todos.map((todo) => (
<li key={todo.id}>
<input defaultValue={todo.text} />
</li>
))}
</ul>
</>
);
}
If you had used key={index} here, typing into the first input and then clicking “Add to top” would leave your typed text attached to the new row, because index 0 now belongs to a different todo. With key={todo.id}, each input stays bound to its own todo. This is the classic symptom: form values, focus, or checkbox state “jumping” to the wrong row.
| Key choice | When it’s safe | Risk |
|---|---|---|
| Stable database/UUID id | Always preferred | None |
| Slug or other unique field | When guaranteed unique and stable | Breaks on duplicates |
| Array index | Static list that never reorders, inserts, or deletes | Wrong state on mutation |
Math.random() | Never | New key every render destroys all state |
Common key bugs
Beyond the index trap, two mistakes recur. First, generating a fresh key on every render (e.g. key={Math.random()} or key={crypto.randomUUID()} inside map) forces React to throw away and recreate every node each render, killing performance and resetting state. Generate ids once when the data is created, not during rendering. Second, duplicate keys—often from a non-unique field—cause React to merge or drop items unpredictably; you’ll see a “two children with the same key” warning.
// Bad: new identity every render
{items.map((item) => <Row key={Math.random()} item={item} />)}
// Bad: keys can collide if two users share a name
{users.map((user) => <Row key={user.name} user={user} />)}
// Good: a stable, unique id
{users.map((user) => <Row key={user.id} user={user} />)}
If your source data genuinely has no stable id, assign one when you load or create the items—{ id: crypto.randomUUID(), ...raw }—and store it in state, so the id persists across renders.
Best Practices
- Render collections with
.map()and return one keyed element per item. - Use a stable, unique id from your data as the key—database ids and UUIDs are ideal.
- Avoid the array index as a key unless the list is static and never reorders, inserts, or deletes.
- Never call
Math.random()orcrypto.randomUUID()inside the render; generate ids once when data is created. - Put the
keyon the outermost element of each iteration, including<Fragment key={...}>when mapping to fragments. - Keep keys unique among siblings only; duplicate keys trigger warnings and lost state.