Keys In Depth
A key is React’s answer to a deceptively hard question: when a list re-renders, which old element corresponds to which new one? React cannot read your mind, so it uses the key prop as a stable identity tag for each item. Get keys right and component state, focus, and DOM nodes follow your data faithfully through reorders, insertions, and deletions. Get them wrong — most commonly by using the array index — and React silently attaches the wrong state to the wrong row.
How React matches old elements to new
During reconciliation React compares the previous tree against the next one. For a list of siblings, it pairs children by their key (within the same position in the parent). If a key exists in both renders, React treats it as the same component instance: it reuses the existing DOM node and preserves that component’s state and effects. If a key disappears, the instance unmounts; if a new key appears, a fresh instance mounts.
Keys only need to be unique among siblings, not globally. They are never passed to your component as a prop — props.key is undefined. They exist purely for React’s bookkeeping.
function ContactList({ contacts }) {
return (
<ul>
{contacts.map((contact) => (
<li key={contact.id}>{contact.name}</li>
))}
</ul>
);
}
Because contact.id is stable and tied to the data, reordering contacts reorders the DOM nodes without recreating them. React moves the existing <li> rather than tearing it down and rebuilding it.
Tip: A good key is stable, unique, and derived from the data itself — a database id, a UUID, a slug. Never generate keys during render with
Math.random()orcrypto.randomUUID(): a brand-new key on every render forces React to remount the item every time, destroying its state and trashing performance.
Why index keys break on reorder and insert
key={index} is the classic trap. It looks fine until the list changes shape, because the index describes a position, not an item. When you insert at the front or reorder, the item at index 0 is now a different piece of data — but React sees the same key and reuses the previous instance’s state for it.
The bug is most visible with uncontrolled state inside each row.
import { useState } from "react";
function Row({ label }) {
const [checked, setChecked] = useState(false);
return (
<li>
<input
type="checkbox"
checked={checked}
onChange={(e) => setChecked(e.target.checked)}
/>
{label}
</li>
);
}
function Demo() {
const [items, setItems] = useState(["Alpha", "Bravo", "Charlie"]);
function addToFront() {
setItems(["Zulu", ...items]);
}
return (
<div>
<button onClick={addToFront}>Add to front</button>
<ul>
{items.map((label, index) => (
// BUG: index keys
<Row key={index} label={label} />
))}
</ul>
</div>
);
}
Check the box next to “Alpha”, then click Add to front. You expect the checkmark to travel with “Alpha”. Instead it stays on the first row, which is now “Zulu”.
Output:
Before: [ ] Zulu <-- (not yet added)
[x] Alpha <-- you checked this one
After: [x] Zulu <-- checkmark stuck to index 0
[ ] Alpha <-- lost its state
[ ] Bravo
[ ] Charlie
React kept the instance whose key was 0 and simply changed its label prop from “Alpha” to “Zulu”. The component state (checked) belonged to position 0, not to “Alpha”. Swap key={index} for key={label} (or a real id) and the checkmark follows the data correctly.
Index keys are only safe when the list is static — never reordered, filtered, inserted into, or deleted from — and the items have no internal state. When in doubt, use a real id.
Resetting component state by changing the key
The same mechanism that preserves state can be used deliberately to reset it. Because React remounts a component when its key changes, giving a component a key tied to “what it represents” forces a clean slate whenever that identity changes.
A common case is a form that should reset its fields when you switch the entity it edits.
import { useState } from "react";
function ProfileForm({ userId }) {
const [draft, setDraft] = useState("");
return (
<input
value={draft}
onChange={(e) => setDraft(e.target.value)}
placeholder={`Notes for user ${userId}`}
/>
);
}
function Editor({ userId }) {
// Changing userId changes the key -> ProfileForm remounts with empty draft.
return <ProfileForm key={userId} userId={userId} />;
}
Without the key, navigating from user 42 to user 99 would leave the half-typed draft in the input. With key={userId}, switching users mounts a fresh ProfileForm, clearing draft automatically — no useEffect cleanup required. This is the idiomatic way to reset state on identity change.
Keys for fragments and array siblings
When you render multiple elements per item, you may need a fragment wrapper that itself carries a key. The shorthand <>...</> cannot take a key, so use the explicit <Fragment> form.
import { Fragment } from "react";
function Glossary({ terms }) {
return (
<dl>
{terms.map((term) => (
<Fragment key={term.id}>
<dt>{term.word}</dt>
<dd>{term.definition}</dd>
</Fragment>
))}
</dl>
);
}
Each Fragment is keyed even though it renders no DOM node of its own, so React can match the <dt>/<dd> pairs across renders. Any time .map() returns elements, every top-level returned element needs a key — including fragments.
Key behavior reference
| Situation | What React does |
|---|---|
| Same key in both renders | Reuses instance; preserves state, refs, effects |
| Key absent in new render | Unmounts the instance |
| New key in new render | Mounts a fresh instance |
| Key changes for a position | Unmounts old, mounts new (state reset) |
| Duplicate keys among siblings | Undefined matching; React warns in dev |
| Missing key in a list | Falls back to index; dev warning logged |
Best Practices
- Use a stable id from your data as the key; reach for the array index only when the list is static and items are stateless.
- Never produce keys with
Math.random()orcrypto.randomUUID()during render — they remount every item on every render. - Keep keys unique among siblings only; they do not need to be globally unique and are not readable as props.
- Use
key={entityId}on a component to intentionally reset its state when the entity it represents changes. - Use the explicit
<Fragment key={...}>form when mapping to multiple sibling elements; the<>shorthand cannot hold a key. - Treat React’s “each child should have a unique key” warning as a real bug, not noise — it predicts state corruption on reorder.