Episode 2 — React Frontend Architecture NextJS / 2.2 — React Components and Props
2.2.e — Keys in React
In one sentence: Keys are special string attributes that help React identify which items in a list have changed, been added, or removed, enabling efficient updates instead of re-rendering the entire list.
Navigation: ← 2.2.d — Rendering Lists · Next → 2.2.f — Reusable Card Component
Table of Contents
- Why React Needs Keys
- How Reconciliation Works
- What Makes a Good Key
- The Index-as-Key Problem
- Proper Key Strategies
- Keys and Component State
- Keys to Force Re-Mount
- Keys in Fragments
- Composite Keys
- Debugging Key Warnings
- Performance Impact — With vs Without Keys
- Key Rules Summary
- Common Mistakes
- Key Takeaways
- Explain-It Challenge
1. Why React Needs Keys
When React re-renders a list, it needs to figure out what changed. Without keys, React has no way to match old items to new items — it can only compare them by position.
┌─────────────────────────────────────────────────────────────┐
│ WITHOUT KEYS — Position-Based Matching │
│ │
│ Before: After (Alice removed): │
│ ┌──────────┐ ┌──────────┐ │
│ │ 0: Alice │ │ 0: Bob │ ← React thinks Alice │
│ ├──────────┤ ├──────────┤ changed TO Bob │
│ │ 1: Bob │ │ 1: Charlie│ ← React thinks Bob │
│ ├──────────┤ └──────────┘ changed TO Charlie │
│ │ 2: Charlie│ ← React thinks Charlie │
│ └──────────┘ was DELETED │
│ │
│ Result: React updates 2 items + deletes 1 │
│ Reality: Only Alice was removed │
│ Wasted work: 2 unnecessary DOM updates │
│ │
│─────────────────────────────────────────────────────────────│
│ WITH KEYS — Identity-Based Matching │
│ │
│ Before: After (Alice removed): │
│ ┌──────────────┐ ┌──────────────┐ │
│ │ key=1: Alice │ │ key=2: Bob │ ← Same key, │
│ ├──────────────┤ ├──────────────┤ no update needed │
│ │ key=2: Bob │ │ key=3: Charlie│ ← Same key, │
│ ├──────────────┤ └──────────────┘ no update needed │
│ │ key=3: Charlie│ │
│ └──────────────┘ ← key=1 is gone, │
│ just remove it │
│ │
│ Result: React removes 1 DOM node (Alice), moves nothing │
│ Optimal! │
└─────────────────────────────────────────────────────────────┘
2. How Reconciliation Works
React's reconciliation algorithm (the "diffing" process) handles lists by comparing children at each position:
Without Keys — O(n) Updates
Step 1: Compare position 0
Old: <li>Alice</li>
New: <li>Bob</li>
→ Different! Update DOM text node from "Alice" to "Bob"
Step 2: Compare position 1
Old: <li>Bob</li>
New: <li>Charlie</li>
→ Different! Update DOM text node from "Bob" to "Charlie"
Step 3: Compare position 2
Old: <li>Charlie</li>
New: (nothing)
→ Remove DOM node
Total: 3 DOM operations (2 updates + 1 deletion)
With Keys — O(1) for Matching
Step 1: Build key map from old list
{ key=1: Alice, key=2: Bob, key=3: Charlie }
Step 2: Walk new list, match by key
key=2 (Bob) → Found in old list, same position? No → move it
key=3 (Charlie) → Found in old list, same position? No → move it
Step 3: Remove unmatched old keys
key=1 (Alice) → Not in new list → remove DOM node
Total: 1 DOM operation (1 deletion, 0 content updates)
The Algorithm Visualized
// React's simplified list reconciliation:
function reconcileList(oldChildren, newChildren) {
const oldKeyMap = {};
// Phase 1: Index old children by key
oldChildren.forEach((child, i) => {
const key = child.key || i; // Fall back to index if no key
oldKeyMap[key] = { element: child, index: i };
});
// Phase 2: Match new children to old by key
newChildren.forEach((child, i) => {
const key = child.key || i;
const oldMatch = oldKeyMap[key];
if (oldMatch) {
// Key exists in both — update if props changed
if (propsChanged(oldMatch.element, child)) {
updateDOM(oldMatch.element, child);
}
delete oldKeyMap[key];
} else {
// New key — insert new DOM node
insertDOM(child, i);
}
});
// Phase 3: Remove old keys not in new list
Object.values(oldKeyMap).forEach(({ element }) => {
removeDOM(element);
});
}
3. What Makes a Good Key
Key Requirements
| Requirement | Description | Example |
|---|---|---|
| Unique among siblings | No two items in the same list can share a key | key={user.id} (IDs are unique) |
| Stable | Same item always gets the same key, across re-renders | Database ID ✅, Math.random() ❌ |
| Not the index | Unless the list is static and never reorders | key={index} is fragile |
Good Keys
// Database ID — best choice
users.map(user => <UserCard key={user.id} user={user} />);
// UUID / nanoid — generated once and stored
items.map(item => <ListItem key={item.uuid} item={item} />);
// Unique natural identifier
countries.map(country => <Option key={country.code} value={country.code}>{country.name}</Option>);
// "US", "UK", "JP" — naturally unique
// Email (if unique in the dataset)
contacts.map(contact => <ContactCard key={contact.email} contact={contact} />);
// Slug (URL-friendly identifier)
blogPosts.map(post => <PostCard key={post.slug} post={post} />);
Bad Keys
// ❌ Math.random() — different on every render! Destroys all performance.
items.map(item => <div key={Math.random()}>{item.name}</div>);
// ❌ Date.now() — same problem, regenerated on each render
items.map(item => <div key={Date.now()}>{item.name}</div>);
// ❌ Index when items can reorder/insert/delete
items.map((item, i) => <div key={i}>{item.name}</div>);
// ❌ Non-unique values (multiple items could share the same name)
items.map(item => <div key={item.name}>{item.name}</div>);
4. The Index-as-Key Problem
Using array indices as keys is the most common mistake. Here's exactly why it breaks.
The Bug: Stateful Components Get Wrong State
function App() {
const [items, setItems] = useState([
{ id: 1, text: "Buy milk" },
{ id: 2, text: "Walk dog" },
{ id: 3, text: "Read book" },
]);
const removeFirst = () => {
setItems(prev => prev.slice(1)); // Remove first item
};
return (
<div>
<button onClick={removeFirst}>Remove First</button>
{items.map((item, index) => (
// ❌ Using index as key
<TodoItem key={index} text={item.text} />
))}
</div>
);
}
function TodoItem({ text }) {
const [checked, setChecked] = useState(false);
return (
<div>
<input
type="checkbox"
checked={checked}
onChange={() => setChecked(!checked)}
/>
{text}
</div>
);
}
What happens when you check "Buy milk" then click "Remove First":
BEFORE (user checks "Buy milk"):
index=0: "Buy milk" ☑ (checked=true)
index=1: "Walk dog" ☐ (checked=false)
index=2: "Read book" ☐ (checked=false)
AFTER (remove first item):
index=0: "Walk dog" ☑ (checked=true) ← BUG! This was "Buy milk"'s state!
index=1: "Read book" ☐ (checked=false)
React sees:
key=0 still exists → reuse component → keep state (checked=true)
key=1 still exists → reuse component → keep state (checked=false)
key=2 disappeared → destroy component
But the text changed from "Buy milk" to "Walk dog", so now
"Walk dog" is checked even though the user never checked it!
Fix: Use Stable IDs
{items.map(item => (
// ✅ key matches the DATA, not the position
<TodoItem key={item.id} text={item.text} />
))}
Now when "Buy milk" (id=1) is removed:
- React sees key=1 is gone → destroys that component and its state
- key=2 ("Walk dog") and key=3 ("Read book") keep their correct states
When Index IS Acceptable
| Scenario | Index OK? | Why |
|---|---|---|
| Static list, never changes | ✅ | Positions are stable |
| Display-only, no state | ✅ | No state to mix up |
| No reordering/insertion/deletion | ✅ | Indices stay consistent |
| Items have no unique ID | ⚠️ Last resort | Better to generate IDs |
| Items can be reordered | ❌ | State will be wrong |
| Items can be inserted/deleted | ❌ | State will be wrong |
| Items have input fields or checkboxes | ❌ | User input gets mixed up |
5. Proper Key Strategies
Strategy 1: Database IDs
// Most common — every database record has a unique ID
function UserList({ users }) {
return users.map(user => (
<UserCard key={user.id} user={user} />
));
}
// user.id could be: 1, 2, 3 (integer) or "abc123" (string)
Strategy 2: Generate IDs on Creation
import { nanoid } from "nanoid";
function TodoApp() {
const [todos, setTodos] = useState([]);
const addTodo = (text) => {
setTodos(prev => [
...prev,
{
id: nanoid(), // Generate ID when creating the item
text,
done: false
},
]);
};
return (
<ul>
{todos.map(todo => (
<li key={todo.id}>{todo.text}</li>
))}
</ul>
);
}
Strategy 3: Natural Unique Identifiers
// Country codes
countries.map(c => <Option key={c.iso3166} ... />);
// File paths
files.map(f => <FileItem key={f.path} ... />);
// URLs
links.map(l => <LinkCard key={l.url} ... />);
// Usernames (if guaranteed unique)
members.map(m => <MemberBadge key={m.username} ... />);
Strategy 4: Crypto.randomUUID() (Modern Browsers)
const addItem = (text) => {
setItems(prev => [
...prev,
{ id: crypto.randomUUID(), text },
// "f47ac10b-58cc-4372-a567-0e02b2c3d479"
]);
};
6. Keys and Component State
Keys have a critical relationship with component state: when a key changes, React destroys the old component and creates a new one from scratch.
Same Key = Preserve State
function App() {
const [user, setUser] = useState({ id: 1, name: "Alice" });
return (
<div>
{/* Same key=1 across renders → state is preserved */}
<EditForm key={user.id} user={user} />
<button onClick={() => setUser({ id: 1, name: "Alice Updated" })}>
Update Name
</button>
</div>
);
}
function EditForm({ user }) {
const [draft, setDraft] = useState(user.name);
// If key stays 1, draft state is KEPT even when user prop changes
// This is actually a bug! (See "Keys to Force Re-Mount" section)
return <input value={draft} onChange={e => setDraft(e.target.value)} />;
}
Different Key = Destroy + Recreate
function App() {
const [userId, setUserId] = useState(1);
const users = { 1: { id: 1, name: "Alice" }, 2: { id: 2, name: "Bob" } };
return (
<div>
{/* When userId changes 1→2, key changes, component is DESTROYED and RECREATED */}
<EditForm key={userId} user={users[userId]} />
<button onClick={() => setUserId(userId === 1 ? 2 : 1)}>
Switch User
</button>
</div>
);
}
function EditForm({ user }) {
const [draft, setDraft] = useState(user.name);
// When key changes, this component is freshly mounted
// draft is re-initialized from user.name — correct behavior!
return <input value={draft} onChange={e => setDraft(e.target.value)} />;
}
7. Keys to Force Re-Mount
This is a powerful technique: change a component's key to force it to reset.
Problem: Form Doesn't Reset When Switching Items
// ❌ BUG: Switching between users doesn't reset the form
function UserEditor({ selectedUserId, users }) {
const user = users.find(u => u.id === selectedUserId);
return <EditForm user={user} />;
// When selectedUserId changes from 1 to 2,
// React sees the same <EditForm> component at the same position
// → it UPDATES props but KEEPS state
// → the form still shows user 1's draft text!
}
// ✅ FIX: Add key={selectedUserId} to force re-mount
function UserEditor({ selectedUserId, users }) {
const user = users.find(u => u.id === selectedUserId);
return <EditForm key={selectedUserId} user={user} />;
// When selectedUserId changes, key changes
// React DESTROYS old EditForm, creates a new one
// All state is fresh — draft is re-initialized from new user
}
Common Use Cases for Key-Based Reset
// Reset form when editing a different item
<EditForm key={item.id} item={item} />
// Reset animation when content changes
<FadeIn key={slideIndex}>
<SlideContent slide={slides[slideIndex]} />
</FadeIn>
// Reset timer when game restarts
<GameTimer key={gameId} duration={60} />
// Reset scroll position when navigating
<PageContent key={currentRoute} route={currentRoute} />
8. Keys in Fragments
When you need to return multiple elements without a wrapper, and each needs a key:
function DefinitionList({ terms }) {
return (
<dl>
{terms.map(term => (
// Can't use <></> shorthand with keys
// Must use <React.Fragment key={...}>
<React.Fragment key={term.id}>
<dt>{term.word}</dt>
<dd>{term.definition}</dd>
</React.Fragment>
))}
</dl>
);
}
// ❌ This won't work — shorthand fragments can't have keys
{terms.map(term => (
<key={term.id}> // Syntax error!
<dt>{term.word}</dt>
<dd>{term.definition}</dd>
</>
))}
// Alternative: import Fragment
import { Fragment } from "react";
{terms.map(term => (
<Fragment key={term.id}>
<dt>{term.word}</dt>
<dd>{term.definition}</dd>
</Fragment>
))}
9. Composite Keys
Sometimes no single field is unique, but a combination is.
// Schedule: same room can have different events on different days
// Neither eventName nor room nor day is unique alone
const schedule = [
{ day: "Monday", room: "A101", event: "Math" },
{ day: "Monday", room: "A102", event: "Science" },
{ day: "Tuesday", room: "A101", event: "English" },
{ day: "Tuesday", room: "A101", event: "History" }, // Same room, same day!
];
// ❌ No single field is unique
schedule.map(item => (
<ScheduleItem key={item.day} ... /> // Duplicate keys!
));
// ✅ Composite key
schedule.map((item, index) => (
<ScheduleItem key={`${item.day}-${item.room}-${index}`} item={item} />
));
// ✅ Even better — use a delimiter that won't appear in data
schedule.map(item => (
<ScheduleItem key={`${item.day}|${item.room}|${item.event}`} item={item} />
));
Generating Composite Keys Safely
function makeKey(...parts) {
return parts.map(p => String(p)).join("::");
}
// Usage
schedule.map(item => (
<ScheduleItem
key={makeKey(item.day, item.room, item.timeSlot)}
item={item}
/>
));
10. Debugging Key Warnings
The Warning
Warning: Each child in a list should have a unique "key" prop.
Common Causes and Fixes
// CAUSE 1: Forgot to add key
// ❌
items.map(item => <div>{item.name}</div>);
// ✅
items.map(item => <div key={item.id}>{item.name}</div>);
// CAUSE 2: Key on wrong element
// ❌ Key is on the inner element, not the outermost
items.map(item => (
<div>
<span key={item.id}>{item.name}</span>
</div>
));
// ✅ Key must be on the outermost element returned by map
items.map(item => (
<div key={item.id}>
<span>{item.name}</span>
</div>
));
// CAUSE 3: Duplicate keys
const items = [
{ id: 1, name: "Apple" },
{ id: 1, name: "Banana" }, // Same id!
];
// Warning: Encountered two children with the same key "1"
// Fix: Ensure IDs are actually unique in your data
// CAUSE 4: Key inside the child component
// ❌ Key can't be accessed inside the component
function ListItem({ key, item }) {
// key is NOT accessible as a prop!
return <div>{item.name}</div>;
}
// ✅ If you need the same value inside, pass it as a separate prop
function ListItem({ id, item }) {
return <div data-id={id}>{item.name}</div>;
}
items.map(item => <ListItem key={item.id} id={item.id} item={item} />);
Key Is NOT a Prop
function Child({ key }) {
console.log(key); // undefined! React consumes key, doesn't pass it
}
<Child key="abc" />
// key="abc" is used by React internally, NOT available in the component
// If you need the same value as a prop, pass it separately:
<Child key={item.id} id={item.id} />
11. Performance Impact — With vs Without Keys
Benchmark Scenario: Remove First Item from a List of 1,000
┌─────────────────────────────────────────────────────────────┐
│ PERFORMANCE: Removing First Item from 1000 │
│ │
│ WITHOUT proper keys (index as key): │
│ ├── Item 0: text "A" → text "B" UPDATE │
│ ├── Item 1: text "B" → text "C" UPDATE │
│ ├── Item 2: text "C" → text "D" UPDATE │
│ ├── ... │
│ ├── Item 998: text "YYY" → text "ZZZ" UPDATE │
│ └── Item 999: removed DELETE │
│ Total: 999 text updates + 1 deletion = 1000 operations │
│ │
│ WITH proper keys (unique ID): │
│ └── key="id_1": removed DELETE │
│ Total: 1 deletion = 1 operation │
│ │
│ Speedup: ~1000x for this operation! │
└─────────────────────────────────────────────────────────────┘
When It Matters Most
| Scenario | Impact of Bad Keys |
|---|---|
| Small static list (5 items) | Negligible |
| Large list (100+ items) with reordering | Severe — hundreds of unnecessary DOM updates |
| List items with input fields | Critical — user data gets mixed between items |
| List items with animations | Visible bugs — animations trigger on wrong items |
| List items with fetch-on-mount | Severe — unnecessary API calls when items "change" |
| Drag-and-drop lists | Broken — items jump to wrong positions |
12. Key Rules Summary
┌─────────────────────────────────────────────────────────────┐
│ KEY RULES CHEAT SHEET │
│ │
│ 1. ALWAYS add keys to list items │
│ items.map(item => <X key={item.id} />) │
│ │
│ 2. Keys must be UNIQUE among siblings │
│ (Different lists can reuse the same keys) │
│ │
│ 3. Keys must be STABLE across renders │
│ ✅ Database IDs, UUIDs, natural identifiers │
│ ❌ Math.random(), Date.now() │
│ │
│ 4. Use index ONLY for static, display-only lists │
│ │
│ 5. Key goes on the OUTERMOST element in map() │
│ │
│ 6. Key is NOT accessible as a prop inside the component │
│ │
│ 7. Change a key to FORCE component re-mount (reset state) │
│ │
│ 8. Use <React.Fragment key={...}> when mapping │
│ to multiple elements │
│ │
│ 9. Composite keys: `${field1}-${field2}` when no │
│ single field is unique │
│ │
│ 10. Generate IDs at creation time, not render time │
└─────────────────────────────────────────────────────────────┘
13. Common Mistakes
Mistake 1: Generating Keys During Render
// ❌ New key every render — component is destroyed and recreated each time!
items.map(item => <Card key={Math.random()} item={item} />);
// This is WORSE than no key at all — forces React to destroy and rebuild
// every component on every render. Animations break. Input values lost.
// State lost. Performance destroyed.
// ❌ Same problem with crypto.randomUUID() in render
items.map(item => <Card key={crypto.randomUUID()} item={item} />);
// ✅ Use a stable identifier that was set when the item was CREATED
items.map(item => <Card key={item.id} item={item} />);
Mistake 2: Non-Unique Keys
// ❌ Multiple users could share the same name
users.map(user => <li key={user.name}>{user.name}</li>);
// If two users are named "John", React only renders one of them
// ✅ Use a guaranteed unique field
users.map(user => <li key={user.id}>{user.name}</li>);
Mistake 3: Keys on the Wrong Element
// ❌ Key on inner span, not on the li
items.map(item => (
<li>
<span key={item.id}>{item.name}</span>
</li>
));
// ✅ Key on the outermost element
items.map(item => (
<li key={item.id}>
<span>{item.name}</span>
</li>
));
Mistake 4: Thinking Keys Affect Rendering Order
// Keys do NOT control display order
// This will still render in array order: Charlie, Alice, Bob
const items = [
{ id: 3, name: "Charlie" },
{ id: 1, name: "Alice" },
{ id: 2, name: "Bob" },
];
items.map(item => <li key={item.id}>{item.name}</li>);
// Renders: Charlie, Alice, Bob (array order, NOT key order)
// To change order, sort the ARRAY first:
[...items]
.sort((a, b) => a.name.localeCompare(b.name))
.map(item => <li key={item.id}>{item.name}</li>);
// Renders: Alice, Bob, Charlie
14. Key Takeaways
- Keys let React track identity — which items are the same across renders
- Without keys, React uses position — causing unnecessary DOM updates and state bugs
- Use stable, unique identifiers — database IDs, UUIDs, natural keys (email, slug, code)
- Never use Math.random() or Date.now() — these change every render, destroying performance
- Index is acceptable ONLY for static, non-reorderable, stateless lists
- The index-as-key bug: stateful components get each other's state when items shift position
- Changing a key forces re-mount — useful for resetting forms, animations, timers
- Keys are consumed by React — they're not accessible as props inside the component
- Use
<React.Fragment key={...}>when mapping to multiple sibling elements - Generate IDs when creating data, not during rendering
Explain-It Challenge
-
The Classroom: Imagine a teacher with 30 students. On day 1, she learns names (keys). On day 2, one student transfers out. With keys (names), she knows exactly who left. Without keys (only seat positions), she thinks 29 students changed seats. How does this relate to React's reconciliation?
-
The Hotel: A hotel has rooms (DOM nodes) and guests (components). When a guest checks out (item removed from list), the hotel knows which room to clean by room number (key). Without room numbers, the hotel would have to check every room. Explain how this maps to React's key-based diffing.
-
The Bug Report: Your colleague shows you this code and says "there's a bug — when I delete a todo, the checkboxes get mixed up." Walk through exactly why
key={index}causes this and howkey={todo.id}fixes it.
todos.map((todo, index) => (
<TodoItem key={index} text={todo.text} />
))
Navigation: ← 2.2.d — Rendering Lists · Next → 2.2.f — Reusable Card Component