π Keys in React
You have been dutifully adding a key to every mapped item. This lesson finally explains what that key is for: it is the identity React uses to match each element between renders. Choose keys well and lists update instantly and correctly; choose them badly β by array index β and you get subtle bugs where the wrong text, checkbox, or focus sticks to the wrong row.
Week 4 · Day 4 (Thursday: Lists and Conditional Rendering) · Lecture 2
π― Learning Objectives
By the end of this lesson, you will be able to:
- Explain what a
keyis and the identity problem it solves for React - Describe how reconciliation matches old and new elements by key
- Walk through a concrete reorder/insert that index keys corrupt β and why
- Choose stable, unique, predictable keys from your data
- Recognize when index-as-key is actually safe
- Diagnose and fix the "missing key" and "duplicate key" warnings
Estimated Time: 55 minutes
Practice: Break, then fix, a reorderable list whose input values jump to the wrong rows.
In This Lesson
What Is a Key?
A key is a string or number you attach to each element in a rendered list. It is not passed to your component and it never appears in the DOM. Its single job is to give each item a stable identity that React can recognize from one render to the next.
Think of a coat check. When you hand over your coat you get a numbered ticket. Later you present the ticket and get your coat back β not whichever coat happens to be hanging in that position now. The key is that ticket. Without it, React can only identify items by where they sit in the list (their position), which falls apart the moment the list changes order.
function TeamList({ members }) {
return (
<ul>
{members.map((member) => (
// member.id is the "coat check ticket" β a durable identity.
<li key={member.id}>{member.name}</li>
))}
</ul>
);
}
π‘ Keys are for siblings, and only for siblings
A key only needs to be unique among its immediate siblings β the items produced by one map() β not globally across the whole app. Two different lists can each contain a key="1" with no conflict.
How Reconciliation Uses Keys
When state changes, your component re-runs and produces a fresh tree of elements. React does not throw away the old DOM and rebuild it β that would be slow and would destroy focus, scroll position, and component state. Instead it runs reconciliation: it compares the new element tree to the previous one and applies the minimal set of DOM changes. For lists, keys are how it lines the two versions up.
to previous by key} D -->|Same key exists| E[Reuse & update in place] D -->|New key| F[Mount a new element] D -->|Key gone| G[Unmount old element] E --> H[Minimal DOM changes] F --> H G --> H
Read that carefully: React matches by key first, then by position. When keys are stable ids, an item that merely moved keeps the same key, so React reuses its DOM node and its component state and simply re-positions it. When keys are missing, React falls back to position β item #1 is compared to old item #1 regardless of whether it is really the same thing.
π What "state" is tied to a key
React associates a component's internal state β useState values, uncontrolled input text, focus, animation progress β with its position and key in the tree. If a key stays the same, that state travels with the item. If the key changes, React treats it as a brand-new element: it throws the old state away and mounts fresh. This is exactly why the wrong key choice corrupts state.
The Index-as-Key Bug
Here is the failure everyone hits once. Consider an editable list where each row has a label and a text input, and a button that inserts a new row at the top. We key by array index.
import { useState } from 'react';
// β BUGGY: keyed by index.
function GuestListBuggy() {
const [guests, setGuests] = useState([
{ name: 'Alice' },
{ name: 'Bob' },
{ name: 'Carol' },
]);
// Insert a new guest at the FRONT of the list.
const addAtTop = () =>
setGuests([{ name: 'NEW' }, ...guests]);
return (
<div>
<button onClick={addAtTop}>Add guest at top</button>
<ul>
{guests.map((guest, index) => (
<li key={index}>
{guest.name}:{' '}
{/* Uncontrolled input β its text lives in the DOM node */}
<input placeholder="meal preference" />
</li>
))}
</ul>
</div>
);
}
Try it in your head. You type "vegan" into Alice's input, "none" into Bob's. Now you click Add guest at top. What should happen: a new empty row appears above Alice, and everyone's typed text stays with the right person. What actually happens with index keys:
Why? The new array is [NEW, Alice, Bob, Carol]. Their index keys are now 0, 1, 2, 3. React compares by key: old key=0 (which owned the input containing "vegan") is matched to new key=0, which is NEW. So the DOM input that holds "vegan" is reused for NEW's row. Alice slides to key=1 and inherits Bob's old input ("none"). Every uncontrolled value is off by one. The labels updated because they come from props, but the input state lives in the DOM node React decided to reuse.
The fix: a stable id
// β
CORRECT: keyed by a stable id that belongs to the guest.
function GuestListFixed() {
const [guests, setGuests] = useState([
{ id: 'a', name: 'Alice' },
{ id: 'b', name: 'Bob' },
{ id: 'c', name: 'Carol' },
]);
const addAtTop = () =>
setGuests([{ id: crypto.randomUUID(), name: 'NEW' }, ...guests]);
return (
<div>
<button onClick={addAtTop}>Add guest at top</button>
<ul>
{guests.map((guest) => (
// The key follows the guest, wherever they move.
<li key={guest.id}>
{guest.name}: <input placeholder="meal preference" />
</li>
))}
</ul>
</div>
);
}
Now Alice's key is "a" whether she is at index 0 or index 1. React matches key="a" to key="a", keeps her exact input node, and simply moves it down. NEW gets a genuinely new key, so React mounts a fresh, empty input for it. Everyone's text stays with the right person.
β οΈ The bug hides until the list changes
Index keys look perfectly fine on the first render and on any render that only appends to the end. The corruption appears only when items are inserted at the front/middle, removed, reordered, filtered, or sorted. That is exactly why it slips through casual testing β and why the safe habit is to always use real ids.
Rules for Good Keys
A good key is unique among siblings, stable across renders, and predictable (derived from the data, not from render order or randomness).
1. Unique among siblings
// Nested lists: each map has its own sibling scope.
function Catalog({ categories }) {
return (
<div>
{categories.map((category) => (
<section key={category.id}>
<h3>{category.name}</h3>
<ul>
{category.items.map((item) => (
// Only needs to be unique within THIS <ul>.
<li key={item.id}>{item.name}</li>
))}
</ul>
</section>
))}
</div>
);
}
2. Stable across renders
// β New key every render β React remounts every item, every time.
{items.map((item) => (
<li key={Math.random()}>{item.text}</li>
))}
// β
Same key persists across renders.
{items.map((item) => (
<li key={item.id}>{item.text}</li>
))}
A key generated with Math.random() or Date.now() inside render changes every time, so React thinks every item is new on each render β it unmounts and remounts the whole list, destroying focus and state and killing performance. Generate ids once, when the item is created, and store them in state.
3. Predictable (from the data)
// β Key depends on a counter that resets each render.
let counter = 0;
{items.map((item) => <li key={++counter}>{item}</li>)}
// β
Key derived from the item itself.
{items.map((item) => <li key={item.sku}>{item.name}</li>)}
No natural id? Compose or generate one
// Composite key when a single field isn't unique but a pair is:
<li key={`${category.id}-${item.name}`}>{item.name}</li>
// Or assign an id at creation time and keep it in state:
function addTodo(text) {
setTodos((prev) => [
...prev,
{ id: crypto.randomUUID(), text }, // generated ONCE, then stable
]);
}
β Where to get ids
Best: an id from your database or API. Next best: crypto.randomUUID() (built into modern browsers and Node) assigned when the item is created. A library like nanoid works too. The key idea is generate once, store, reuse β never regenerate during render.
When Index Keys Are Fine
Index-as-key is not forbidden β it is fine when all three of these hold, because then position is a stable identity:
- The list is static β items are never reordered, inserted, or removed.
- Items have no stable id of their own to use instead.
- List items hold no local state (no inputs, no toggles, no focus to preserve).
// β
OK: a fixed set of instructions that never changes order or content.
function Steps() {
const steps = [
'Preheat the oven to 200Β°C',
'Mix the dry ingredients',
'Fold in the wet ingredients',
'Bake for 25 minutes',
];
return (
<ol>
{steps.map((step, index) => (
<li key={index}>{step}</li>
))}
</ol>
);
}
π‘ A safe default rule
When in doubt, use a real id. Index keys buy nothing except a bug waiting for the day the list becomes dynamic. Reserve them for genuinely static, stateless lists β and even then, a stable id is never wrong.
Fixing Key Warnings
React surfaces two common key problems in the console. Learn to read them.
"Each child in a list should have a unique key prop"
// β Cause: no key at all.
{items.map((item) => <li>{item.name}</li>)}
// β
Fix: add a stable key.
{items.map((item) => <li key={item.id}>{item.name}</li>)}
"Encountered two children with the same key"
// β Cause: duplicate ids in the data.
const items = [
{ id: 1, name: 'Apple' },
{ id: 1, name: 'Banana' }, // duplicate!
];
// β
Fix: ensure ids are unique, or compose a unique key.
<li key={`${item.id}-${item.name}`}>{item.name}</li>
Duplicate keys are worse than missing ones: React cannot tell the two items apart, so updates can land on the wrong element. A quick way to audit your data in development:
const keys = items.map((item) => item.id);
console.log('Duplicate keys?', keys.length !== new Set(keys).size);
Console output
Warning: Encountered two children with the same key, `1`.
Keys should be unique so that components maintain their identity
across updates.
Practice & Quiz
ποΈ Exercise 1: Fix the reorder bug
Goal: This checklist lets you delete any item, but it is keyed by index, so deleting the first item leaves the wrong checkboxes ticked. Give it stable keys.
function Checklist() {
const [tasks, setTasks] = useState([
{ text: 'Buy milk', done: false },
{ text: 'Walk dog', done: true },
{ text: 'Write code', done: false },
]);
const remove = (index) =>
setTasks(tasks.filter((_, i) => i !== index));
return (
<ul>
{tasks.map((task, index) => (
<li key={index}> {/* β fix this */}
<input type="checkbox" defaultChecked={task.done} />
{task.text}
<button onClick={() => remove(index)}>β</button>
</li>
))}
</ul>
);
}
π‘ Hint
The task objects have no id yet. Add one when you build the initial state (e.g. id: crypto.randomUUID()), key by task.id, and remove by id rather than by index.
β Solution
function Checklist() {
const [tasks, setTasks] = useState([
{ id: 't1', text: 'Buy milk', done: false },
{ id: 't2', text: 'Walk dog', done: true },
{ id: 't3', text: 'Write code', done: false },
]);
const remove = (id) =>
setTasks(tasks.filter((t) => t.id !== id));
return (
<ul>
{tasks.map((task) => (
<li key={task.id}>
<input type="checkbox" defaultChecked={task.done} />
{task.text}
<button onClick={() => remove(task.id)}>β</button>
</li>
))}
</ul>
);
}
ποΈ Exercise 2: Compose a key
Goal: You render seats as { row: 'A', number: 1 }. Neither field is unique alone, but the pair is. Produce a correct key.
β Solution
function SeatMap({ seats }) {
return (
<ul>
{seats.map((seat) => (
<li key={`${seat.row}-${seat.number}`}>
Seat {seat.row}{seat.number}
</li>
))}
</ul>
);
}
π― Quick Quiz
Question 1: What does React primarily use a list item's key for?
Question 2: Using the array index as a key is most likely to cause bugs whenβ¦
Question 3: Why is key={Math.random()} a bad idea?
Best Practices & Pitfalls
β Do
- Prefer a stable id from your data (database id, SKU, UUID)
- Generate ids once at creation (
crypto.randomUUID()) and store them in state - Compose a key from multiple fields when no single field is unique
- Keep keys unique within each list, not across the whole app
β Don't
- Use the array index for dynamic or stateful lists
- Generate keys with
Math.random()orDate.now()during render - Reuse the same id for two items (duplicate-key warning)
- Try to read
keyas a prop inside the child β it isn't one; pass an explicitidif needed
β οΈ Changing a key on purpose remounts a component
Occasionally you want React to throw away state β for example, resetting a form when the selected user changes. Giving a component a key that changes with the user (<UserForm key={userId} />) forces a fresh mount. It is a powerful, deliberate trick β the same mechanism that causes the index bug, used on purpose.
Summary
π Key Takeaways
- A key gives each list item a stable identity React matches on across renders
- Reconciliation matches by key first, then position; matched items are reused, moved, or removed
- Component and uncontrolled-input state follows the key β so a wrong key attaches state to the wrong item
- Index keys corrupt stateful lists on insert/remove/reorder; use stable ids instead
- Good keys are unique, stable, and predictable; generate ids once and store them
π Additional Resources
- react.dev β Keeping list items in order with key
- react.dev β Preserving and Resetting State
- react.dev β Resetting state with a key
π What's Next?
Lists decide how many of something to render. The next lesson, Conditional rendering patterns, decides whether to render at all: the && operator (and its zero trap you saw earlier), the ternary, early returns, and element variables β the four tools for showing the right UI for the current state.
π Keys, demystified!
You now understand the one prop most React beginners copy without understanding β and you can spot the index-key bug before it ships.