Topic 3.3
Keys: How React Tells Items Apart
In one line
A key is a label you give each item in a list so React can match it with the same item on the next render; keys must be unique among siblings and stable, which is why they come from your data's ids and not from the index or Math.random().
Think of it like this
Coats in a cloakroom. When you hand over your coat you get a numbered token. If the attendant moves the coats around, you still get *your* coat back, because the token belongs to the coat, not to the hook. If tokens were "the third hook from the left", shuffling the hooks would hand you someone else's coat. A key is the token: it belongs to the item, so React finds the right one even when the list moves.
Words you'll meet
New words in this topic, in plain English. Come back here whenever one feels fuzzy.
- Key
- A special prop (
key) that labels an item in a list so React can recognise it on the next render. - Siblings
- Elements that share the same parent, like the
<li>s in one<ul>. - Unique among siblings
- No two items in the same list have the same key. Other lists may reuse it.
- Stable key
- A key that stays the same for the same item on every render.
- Index
- An item's position in an array: 0 for the first, 1 for the second, and so on.
- Reconciliation
- React comparing the new elements with the old ones to decide what to keep, move, add and remove.
- Identity
- Which item an element *is* across renders. React decides identity by type, position and key.
- Fragment
- A wrapper that groups elements without adding a DOM element.
<Fragment key=...>is the form that takes a key. - crypto.randomUUID()
- A browser function that makes a random unique id string, useful when you create a new item.
Step by step
01Two renders of the same list
React has the old list (Asha, Ravi) on screen. You add Meera at the top. React now has to pair up three new rows with two old ones before it touches the DOM.
02With index keys, React pairs by position
The new first row (Meera) gets key 0, which used to belong to Asha. So React keeps Asha's old row, with Asha's typed note and Asha's state, and only changes its label to Meera. Every row shifts the same way, and a new row is created at the end.
03With id keys, React pairs by identity
Asha has key 1 before and after. React moves her existing row, note and all, down one place, and creates one new row for Meera (key 3). Nobody's state moves to someone else.
04Give items an id when you create them
Data from a server usually has ids already. For items made in the browser, create the id once, in the event handler, and store it in the item.
function handleAdd(text) {
const todo = { id: crypto.randomUUID(), text, done: false };
setTodos([...todos, todo]);
}
// render:
{todos.map((t) => <TodoRow key={t.id} todo={t} />)}05Several elements per item: a keyed Fragment
A <dl> needs <dt> and <dd> as direct children, so you can't wrap each pair in a <div>. A Fragment groups them without adding an element, and the long form takes a key.
import { Fragment } from "react";
{terms.map((t) => (
<Fragment key={t.id}>
<dt>{t.word}</dt>
<dd>{t.meaning}</dd>
</Fragment>
))}Try it yourself
- 1
Do the bug by hand
In "The bug", press the reset arrow, delete the demo script at the bottom, and run it. Type a note next to Ravi in both lists, then click Add Meera to the top. Predict where each note ends up before you click.
- 2
Try Math.random() as a key
In the stars example, change the right-hand list to
key={Math.random()}and delete the demo script. Click a star a few times, then click Reverse order. Predict first: every star count goes back to 0, because every row got a new key and React built new rows with fresh state. - 3
Fix the warning two ways
In "The missing-key warning", first add
key={p.id}. Then remove it and instead try puttingkey={product.id}on the<li>insideProductRow. Does the warning come back? (Yes: the key must be on the element in the array,<ProductRow>.) - 4
Try a key on the short Fragment
In the glossary, replace
<Fragment key={t.id}>with<key={t.id}>. The code doesn't even compile: the short<>syntax has no place for attributes.
Code & diagrams
The demo typed **likes tea** next to Asha in both lists and pressed the button. On the left (index keys) the note now sits next to **Meera**; on the right (id keys) it stayed with **Asha**. Press the reset arrow and try it by hand: type next to Asha, then click the button.
Preview
Console
Nothing logged yet.Expected console output
key = index: the note is now next to Meera
key = id: the note is now next to AshaAsha got one star in both lists, then the order was reversed. With index keys the star stayed in the first row, which is now **Meera**. With id keys it moved with **Asha**. State belongs to the key, not to the data you pass in.
Preview
Console
Nothing logged yet.Expected console output
key = index: Meera ★ 1, Ravi ★ 0, Asha ★ 0
key = id: Meera ★ 0, Ravi ★ 0, Asha ★ 1The warning names `ProductList`, the component whose render created the array. Add `key={p.id}` to `<ProductRow>` and it disappears.
Preview
Console
Nothing logged yet.Expected console output
Each child in a list should have a unique "key" prop.
Check the render method of `ProductList`. See https://react.dev/link/warning-keys for more information.The Fragment leaves no element behind: the `<dl>` holds `dt`/`dd` pairs directly, which is what valid HTML requires.
Preview
Console
Nothing logged yet.Expected console output
children of <dl>: DT DD DT DD DT DDBreak it on purpose
Errors are the best teachers. Make each change, read the error, guess what went wrong, then reveal the answer.
Break #1
Two items with the same key
In "Keys on Fragments", change the id of the second term from "prop" to "jsx".
Break #2
Read key inside the child
In the missing-key example, add key={p.id} and change ProductRow to function ProductRow(props) { return <li>{props.key} {props.product.name}</li>; }.
Break #3
Silence the warning with the index
In the missing-key example, write products.map((p, i) => <ProductRow key={i} product={p} />).
Myth vs fact
Myth
Keys must be unique across the whole page.
Fact
Only among siblings in the same list. Two separate lists can both have an item with key 1.
Myth
Using the index as a key fixes the warning, so it fixes the problem.
Fact
The index is what React uses when there is no key. It hides the warning but keeps the bug whenever items are inserted, removed, filtered or reordered.
Myth
Keys are a performance trick you can skip.
Fact
Keys are about correctness: they decide which state and DOM belong to which item. The speed gain from moving instead of rebuilding rows is a bonus.
Myth
Math.random() or crypto.randomUUID() in render gives good unique keys.
Fact
They are unique but change every render, so React rebuilds every row and loses all their state. Generate the id once, when the item is created, and store it.
Pro corner
Extra depth for experienced readers. New to this? Skip it for now and come back later.
- ▸
Keys are part of an element's identity: React matches children by
(type, key). Without an explicit key, React uses the child's index in the array. Sokey={index}and no key behave identically; the only difference is the warning. - ▸
A key also works on a single element, not only in lists: changing
<Profile key={userId} />makes React unmount the oldProfileand mount a new one with fresh state. This is the standard way to reset a component's state when an id changes (Topics 6.4 and 7.6). - ▸
React's child reconciliation walks old and new children with keys in one pass plus a map lookup for moved items, so it is O(n). Without stable keys, an insert at the top makes React update every row's props and text instead of inserting one node; with keys it inserts one DOM node and moves nothing else (Topic 16.2).
- ▸
Uncontrolled DOM state (an input's value, focus, a
<details>being open, CSS transitions, a playing<video>) is even easier to lose than React state, because React doesn't know about it at all. Wrong keys move it with the DOM node, which belongs to the position.
Remember this
- 1
Why React needs keys: after each render React compares the new list of children with the old one (reconciliation, Topic 0.2) to decide what to keep, move, add or remove. Each list item may own things that live outside your data: its state (from
useState), its DOM nodes (with typed text, focus, scroll position, a playing video). React must know which new item is "the same" as which old one so it can keep those things with it. Without keys it can only match by position: first with first, second with second. - 2
The key rules. A key is the special
keyprop on the element returned frommap:<li key={todo.id}>. (1) Unique among siblings: no two items in the *same* list may share a key. Keys don't need to be unique across the whole app; two different lists may both have an item with key1. (2) Stable: the same item must get the same key on every render. A key that changes makes React think the old item left and a brand new one arrived. Keys are compared as strings, so1and"1"count as the same key. - 3
Where keys come from. Best: an id that is already in the data, such as a database id (
user.id,"order-1042"). If you create items in the browser, give each one an id when you create it, in the event handler, not during render: a counter (nextId++) orcrypto.randomUUID(). If the items are unique strings themselves (a fixed list of tags), the string can be the key. The key is not passed to your component:props.keyisundefinedand React warns if you read it. If the child needs the id, pass it again as a normal prop:<Row key={p.id} id={p.id} />. - 4
Why not the index?
items.map((item, i) => <Row key={i} />)gives each row the key "my position". That is exactly what React does with no key at all, so it hides the warning without fixing anything. It is fine for a list that never changes order, never gets items inserted or removed except at the end, and whose rows hold no state. But insert an item at the top, delete one, sort or filter, and every key now points at a different item. React keeps each row's state and DOM with the position, so typed text, ticked boxes and counters stay put while the data under them moves: state sticks to the wrong row. You'll watch it happen below. - 5
**Why not
Math.random()?**key={Math.random()}gives every item a new key on every render. Nothing ever matches, so React throws away every row and builds it again each time: all row state resets, inputs lose focus and typed text while you type, and the list is slower. Keys must be created once and stored with the data, never computed fresh in render. - 6
The missing-key warning. When you render an array of elements without keys, React's development build prints
Each child in a list should have a unique "key" prop.and names the component whose render made the list (Check the render method of ...). Duplicate keys printEncountered two children with the same key. Both are removed in production builds, so fix them while developing. Keys on Fragments: when one item renders several elements with no wrapper (a<dt>and a<dd>in a definition list), you need a Fragment, and the short<>syntax cannot take attributes. ImportFragmentand write<Fragment key={t.id}>...</Fragment>.
Explain it without notes
What is a key in React, and why does React need one for list items?
What are the rules for a good key? Where should keys come from?
Why is using the array index as a key a problem? When is it acceptable?
What goes wrong with key={Math.random()}?
How do you key an item that renders several sibling elements without a wrapper?
Practice
Fix this list so it has no warning and no state bug: todos.map((t, i) => <TodoRow key={i} todo={t} />), where each todo is { id, text }.
Write a NewTodo handler that adds { text } typed by the user to a todos state array with a proper key, and the matching map.
Render a table body from rows = [{ id, name, score }] where each item becomes two <tr> elements: one with the name and one with the score. Key it correctly.
Trade-offs
- ↔
Index keys are acceptable for truly static lists and save you from inventing ids; the moment a list can change order or hold state, the cost is a silent correctness bug, so default to ids.
- ↔
Changing a key on purpose is a cheap way to reset a component, but it throws away its DOM and state completely, which can be more work than updating it.
Done when you can
I can explain why React needs keys to match list items between renders.
I give every list item a unique, stable key from its data.
I can show and explain the index-key bug where state sticks to the wrong row.
I know why
Math.random()keys reset every row.I can read the missing-key and duplicate-key warnings and fix them.
I can put a key on a
Fragmentwhen an item has several elements.