B7 · Part 1 · Beginner
Lists with .map() and keys
A key is not for you and it is not for performance — it tells React which item is which.
10On this page
Rendering a list
There is no loop syntax in JSX, because JSX holds expressions and a
for loop is a statement. You map an array of data to an array of
elements, and React renders arrays by rendering each item.
<ul>
{tasks.map((task) => (
<li key={task.id}>{task.title}</li>
))}
</ul>
Watch the brackets: ( after the arrow returns the JSX. Using
{ opens a function body, and without a return it
produces undefined and an empty list — a five-minute bug
everyone hits once.
What a key is for
When a list re-renders, React has an old array of elements and a new one. It needs to decide, for each new element, whether it corresponds to an existing DOM node it can update, or a new node it must create. The key is that identity.
Without keys, React falls back to position: first item to first item, second to second. That is correct only if the list never reorders, never has items inserted, and never has items removed from anywhere but the end.
Why index keys break
{items.map((item, i) => <Row key={i} item={item} />)}
Start with [A, B, C] — keys 0, 1, 2. Delete A. Now you
have [B, C] with keys 0, 1. React looks at key 0: it existed
before, so it keeps that DOM node and just changes the props from A
to B.
Everything React was told to preserve is preserved on the wrong item: text typed into an input, a checked checkbox, focus, scroll position, and any state inside that row. The list looks right until a row has state, and then it is baffling.
Where to find a key
| Situation | Key |
|---|---|
| Data from a database or API | The record's id. Always. |
| Created in the browser | Generate one at creation: crypto.randomUUID() |
| A genuinely static list | The index is fine — but so is not looping at all |
| No natural id | A stable field or combination, e.g. country + "-" + city |
Generate ids when the item is created, not while rendering.
key={Math.random()} or key={crypto.randomUUID()}
inside map produces a new key every render, so React throws away
every DOM node and rebuilds the entire list each time — worse than no
key at all.
The rules
- Unique among siblings, not globally. Two different lists can both use key
1. - Stable across renders. The same item keeps the same key for its whole life.
- On the outermost element of the mapped item — the thing
mapreturns.
{rows.map((row) => (
<Fragment key={row.id}> // ✓ key on the fragment
<dt>{row.term}</dt>
<dd>{row.def}</dd>
</Fragment>
))}
The short <> syntax cannot take a key, so import
Fragment when you need one.
A key is not a prop
function Row({ key, item }) { // ✗ key is always undefined here
function Row({ id, item }) { // ✓ pass it again if you need it
<Row key={item.id} id={item.id} item={item} />
React consumes key itself; it never reaches the component.
The trick worth knowing
Changing a key deliberately is how you tell React "this is a different thing now" — it unmounts the old component and mounts a fresh one, resetting all of its state.
<ProfileForm key={userId} user={user} />
Switch users and the form resets, without a single effect watching
userId. This is the intended answer to "how do I reset state when
a prop changes", and it is much less code than the alternative.
What React does with a key, step by step
When it reconciles a list, React builds a map of the old children by key, then walks the new list:
- Key exists in the old map and the element type matches → update that DOM node in place, keeping its state.
- Key exists but the type changed → unmount the old, mount a new one.
- Key is new → mount.
- Old key not claimed by anything → unmount.
Reordering a keyed list therefore moves DOM nodes rather than rebuilding them, which is why a keyed list keeps input values and focus across a sort and an index-keyed one does not.
Lists inside lists
{groups.map((group) => (
<section key={group.id}>
<h3>{group.name}</h3>
<ul>
{group.items.map((item) => (
<li key={item.id}>{item.label}</li> // only unique within this ul
))}
</ul>
</section>
))}
Keys are scoped to siblings, so the inner list does not need
group.id + item.id — the items are only ever compared with
each other.
Filtering and sorting
{tasks
.filter((t) => !t.done)
.sort((a, b) => a.due - b.due)
.map((t) => <Row key={t.id} task={t} />)}
This is safe because filter already made a copy —
sort mutates, so sorting tasks directly would mutate
your state in place. If there is no filter in the chain, copy
first: [...tasks].sort(...).
Keep the pipeline readable. Once it grows past two or three steps, pull it out into a named variable above the JSX — and if it is expensive, into useMemo.
The empty case
{tasks.length === 0 ? (
<p className="empty">Nothing due. Add a task to get started.</p>
) : (
<ul>{tasks.map((t) => <Row key={t.id} task={t} />)}</ul>
)}
[].map() renders nothing at all, which is silently wrong —
the user sees a blank area and cannot tell whether it is loading, broken, or
genuinely empty.
Interview answer, one sentence
"Keys give React item identity between renders so it can match elements to existing DOM nodes. Index keys break the moment the list reorders or an item is removed from the middle, because React then preserves state on the wrong row — and changing a key on purpose is the idiomatic way to reset a component."
Opens in the editor: write it, run it, and check it against real tests.
All 46 for this level →Finished reading?
0 of 57 chapters read in this series