In one minute
When you render a list with .map(), React has to work out, on the next render, which new item corresponds to which old one. A key answers that: it’s the item’s identity among its siblings. With a stable id as the key, React moves, keeps and deletes the right rows. With no key or the array index, React matches by position, which goes wrong as soon as items are added, removed or reordered: anything the DOM remembers (typed text, focus, a component’s state) stays at the old position and ends up on the wrong item.
You’ll be able to: choose a key, explain why index keys break, and use key on purpose to reset a component.
1function Cart() {2 const [items, setItems] = useState(initial)3 return items.map((item, index) => (4 <li key={index}>5 <span>{item.name}</span>6 <input placeholder="Gift note" />7 <button onClick={() => remove(item.id)}>Remove</button>8 </li>9 ))10}- event
<button onClick={() => remove(item.id)}>Remove</button>Click Remove on the Ceramic Mug. The list now has two items: Headphones, Backpack. - render phase
<li key={index}>With index keys, the Headphones now have key0, the key the Mug’s row had. So React thinks row 0 is still the same row. - commit phase
<span>{item.name}</span>React keeps row 0’s DOM, its input included, and only changes the text. The last row is deleted. - The Mug’s gift note now sits next to the Headphones. Recorded:
{ item: "Wireless Headphones", note: "Gift wrap, please" }. No key at all gives the same wrong result.
1function Cart() {2 const [items, setItems] = useState(initial)3 return items.map((item, index) => (4 <li key={item.id}>5 <span>{item.name}</span>6 <input placeholder="Gift note" />7 <button onClick={() => remove(item.id)}>Remove</button>8 </li>9 ))10}- event
<button onClick={() => remove(item.id)}>Remove</button>The same click. - render phase
<li key={item.id}>With id keys, React sees thatp1is gone andp2andp3are still there. - commit phaseThe Mug’s row is removed, with its input and its note. The other rows keep their DOM untouched.
- Recorded:
{ item: "Wireless Headphones", note: "" }. Every note stays with its product.
The example: a cart with gift notes
function Cart({ keyBy }: { keyBy: 'index' | 'id' | 'none' }) {
const [items, setItems] = useState(initial)
const remove = (id: string) => setItems((list) => list.filter((p) => p.id !== id))
return (
<ul className="cart">
{items.map((item: Product, index) => (
<li key={keyBy === 'id' ? item.id : keyBy === 'index' ? index : undefined}>
<span>{item.name}</span>
<input placeholder="Gift note" />
<button onClick={() => remove(item.id)}>Remove</button>
</li>
))}
</ul>
)
}
The recorder typed “Gift wrap, please” next to the Ceramic Mug, then removed the Mug. The rows afterwards:
With key={item.id}:
[
{
"item": "Wireless Headphones",
"note": ""
},
{
"item": "Trail Backpack",
"note": ""
}
]
With key={index}:
[
{
"item": "Wireless Headphones",
"note": "Gift wrap, please"
},
{
"item": "Trail Backpack",
"note": ""
}
]
With no key at all (React also warned):
[
{
"item": "Wireless Headphones",
"note": "Gift wrap, please"
},
{
"item": "Trail Backpack",
"note": ""
}
]
Each child in a list should have a unique "key" prop.
Only the id version is right. With index keys, the Headphones got key 0, the key the Mug’s row used to have, so React kept that row (and its input with the note) and just changed its text. No key behaves exactly like index keys, plus a warning.
Rules for keys
- Unique among siblings, not across the app. Two different lists can both use
p1. - Stable: the same item gets the same key on every render. Never
Math.random()orDate.now()inside.map(): a new key each render means a brand-new element each time, so React remounts every row and loses its state. - From the data: a database id, a SKU, a slug. Index keys are only acceptable for lists that never reorder, filter or insert, and whose rows hold no state.
- Keys aren’t props. The component never receives
key; pass the id separately if it needs it (Raw React APIs).
key as a reset button
Changing an element’s key tells React “this is a different element”: it unmounts the old one (and its state) and mounts a fresh one. That’s an easy way to reset a component without writing reset logic:
function Coupon() {
const [resetKey, setResetKey] = useState(0)
return (
<>
<input key={resetKey} id="coupon" placeholder="Coupon code" />
<button id="clear" onClick={() => setResetKey((k) => k + 1)}>Clear</button>
</>
)
}
{
"logs": [],
"warnings": [],
"before": "SAVE10",
"after": ""
}
The same trick clears a file input, or resets a whole form or profile page when the selected product id changes: <ProductEditor key={productId} />.
Common mistakes
key={index}on lists that can change.- Generating keys during render (
key={crypto.randomUUID()}). - Putting the key on the wrong element: it belongs on the outermost element returned from
.map()(often a<li>or a component), not inside it.
Interview Q&A
Why does React need a key when rendering arrays?
To match items between renders. Without identity, a removed first item looks the same as “every item’s content shifted up by one”, and React updates by position. Keys let it keep, move or delete the element that actually belongs to each item.
Why is the array index usually a bad key?
The index is the position, so it gives React no new information. Recorded: after removing the Mug with index keys, its gift note stayed on row 0, which now showed the Headphones.
What makes a valid key?
Unique among the siblings in that list, and stable for the same item across renders. Usually an id from the data.
What else can changing a key be used for?
Resetting state: a new key unmounts the old element and mounts a new one. Recorded: a coupon input with key={resetKey} went from "SAVE10" to "" when the key changed.
Related
- Reconciliation and Child Reconciliation Algorithm (React Internals): exactly how React compares keyed and unkeyed lists.
- Inputs: resetting a file input.
Sources
- react.dev: Rendering lists, Preserving and resetting state
- Topic order inspired by Kent C. Dodds’ EpicReact React Fundamentals workshop; the example app and code here are this handbook’s own.
Primary source: react.dev/learn/rendering-lists