Keys and lists
A key tells React which item is which between renders. Why index keys move state onto the wrong row, what makes a good key, and using a key to reset a component.
After this lesson you can
- Explain what React uses a key for when it reconciles a list
- Show how an index key attaches state to the wrong item after an insert
- Choose a stable key, and use a key change to reset a component on purpose
Rendering a list is a map that returns elements:
<ul>
{todos.map((todo) => (
<TodoRow key={todo.id} todo={todo} />
))}
</ul>
The previous lesson matched children by position. For a list that can be
reordered, filtered or prepended to, position is the wrong identity: the
third row today may be a different todo from the third row yesterday. A
key replaces position as the identity. Among siblings, React matches
the new child with key "42" to the old child with key "42", wherever
each of them sits, and that instance keeps its state and DOM node. Keys
that disappear are unmounted; new keys are mounted.
Why the index is a bad key
Using the array index as the key (or giving no key, which falls back to the index and logs a warning) is the same as positional matching. It works until the list changes anywhere but the end. Here each row holds some local state — an unsaved draft in an input — and a new item is added to the top:
// Each mounted row keeps its own local "draft" state, stored by key.function rerender(prevStates, items, keyOf) { const next = {}; return items.map((item, index) => { const key = keyOf(item, index); const draft = key in prevStates ? prevStates[key] : ""; next[key] = draft; return `${item.text} [draft: "${draft}"]`; });} export function run() { const before = [ { id: 7, text: "Write report" }, { id: 3, text: "Buy milk" }, ]; const after = [{ id: 9, text: "Call Sam" }, ...before]; // State the user typed while the first list was on screen: const byIndex = { 0: "draft for report", 1: "2 litres" }; const byId = { 7: "draft for report", 3: "2 litres" }; return { indexKeys: rerender(byIndex, after, (item, i) => i), idKeys: rerender(byId, after, (item) => item.id), };}With index keys, every row was matched with whatever sat at its index before: the brand-new "Call Sam" row took the draft that belonged to "Write report", "Write report" (now at index 1) took the "2 litres" draft that belonged to "Buy milk", and "Buy milk", at an index that did not exist before, started empty. The data is right and the state is attached to the wrong rows. With id keys every draft stays with its own item, and only the new row starts empty.
What makes a good key
- Stable: the same item gets the same key on every render. An id from
your data is ideal.
Math.random()orcrypto.randomUUID()called during render produces a new key every time, so every row remounts on every render. - Unique among siblings: keys only need to differ within one list, not across the whole app.
- Not derived from position: the index is fine only for a static list that never reorders, filters or inserts.
The key goes on the element returned from map, not on something inside
it. If map returns several elements per item, wrap them in
<Fragment key={id}> (the short <> syntax cannot take a key). And a
component cannot read its own key — pass the id as a separate prop if it
needs it.
A key resets state on purpose
Since a different key means a different instance, changing a key is the
cleanest way to reset a component. <ProfileForm key={userId} /> gives
each user a fresh form, with no effect copying props into state.
Common mistakes
- Index keys on a list that changes. After an insert, filter or sort, state (input text, checked boxes, open panels) sticks to positions instead of items, as the example models.
Exercise · Pro
Match a keyed list
Exercises are free in the first 3 lessons of every course. From here on they are part of Pro and Sprint; the lesson and its runnable examples stay open.
Upgrade