map returns elements
map returns an array; React renders each element in it. forEach returns undefined, which renders nothing — the commonest reason a list comes out blank.
<ul>
{doors.map((door) => (
<li key={door.id}>{door.name}</li>
))}
</ul>
What a key is for
When the list re-renders, React has to match the new elements to the existing DOM nodes. The key is how it does that: same key means "this is the same item, update it"; a key that was not there before means "this is new, create it".
Without keys React falls back to position, which is fine until the order changes.
{doors.map((door, i) => <Row key={i} door={door} />)}
// remove the FIRST door:
// before: key 0 → Painted, key 1 → Veneer
// after: key 0 → Veneer
// React thinks item 0 changed its name, and REUSES the first row's DOM
// — so anything not derived from props stays: input values, focus, animation
Choosing one
| Key | Use it when |
|---|---|
item.id | Almost always. A database id, a uuid, a slug. |
A composite: ` ${a}-${b} ` | No single field is unique but a pair is. |
| The value itself | A list of unique strings that never repeat. |
| The index | Only when the list is static, never reordered, filtered or edited. |
key={Math.random()} or key={crypto.randomUUID()} gives every item a new key every render, so React throws away and rebuilds the whole list each time — losing focus, scroll and state, and doing the maximum possible work.
Where the key goes
// right
{doors.map((door) => <Row key={door.id} door={door} />)}
// wrong: the key is inside the component, not on the repeated element
function Row({ door }) {
return <li key={door.id}>{door.name}</li>;
}
// with a fragment that needs a key, use the long form
{doors.map((door) => (
<React.Fragment key={door.id}>
<dt>{door.name}</dt>
<dd>{door.days}</dd>
</React.Fragment>
))}
Filtering and sorting
A filtered list is derived state: it is computed from doors and query. Keeping it in useState and updating it in an effect is the single most common mistake in React, and it guarantees the two will disagree at some point.
function Doors({ doors, query, sort }) {
const shown = doors
.filter((d) => d.name.toLowerCase().includes(query.toLowerCase()))
.sort((a, b) => (sort === "days" ? a.days - b.days : a.name.localeCompare(b.name)));
if (shown.length === 0) return <p className="empty">Nothing matches “{query}”.</p>;
return <ul>{shown.map((d) => <li key={d.id}>{d.name}</li>)}</ul>;
}
doors.sort(…) sorts the prop array in place — the parent's data. Use toSorted, or [...doors].sort(…).
Empty and long lists
- Always handle the empty case, and say what to do next.
- A few hundred rows is fine. A few thousand needs virtualisation — render only the visible ones.
content-visibility: autoin CSS gets you a long way before reaching for a library.
Nested lists
Keys only have to be unique among siblings, not across the page.
{groups.map((group) => (
<section key={group.id}>
<h2>{group.name}</h2>
<ul>
{group.doors.map((door) => (
<li key={door.id}>{door.name}</li>
))}
</ul>
</section>
))}
A key can also force a reset
This is the one place where changing a key on purpose is the right answer — it says "this is a different thing now", and everything inside starts over.
// changing the key unmounts the old component and mounts a fresh one,
// which is the cleanest way to clear a form when the subject changes
<EditForm key={selectedId} door={selected} />