What Are Keys?
Keys are special string attributes you provide when rendering lists of elements. They tell React which item in a list corresponds to which item in the previous render.
function TodoList({ todos }) {
return (
<ul>
{todos.map(todo => (
<li key={todo.id}>{todo.text}</li>
))}
</ul>
);
}
Without keys, React does not know if an item moved, was added, or was removed. It falls back to comparing by position, which leads to bugs and wasted work.
How React Reconciliation Works
When state changes and React re-renders, it builds a new virtual DOM tree and compares it to the previous one. This process is called reconciliation. For lists, React needs a way to match elements between the old and new trees.
Old list: New list (item B removed):
+---------+ +---------+
| key="A" | | key="A" | <-- matched, reuse DOM node
+---------+ +---------+
| key="B" | | key="C" | <-- matched, reuse DOM node
+---------+ +---------+
| key="C" | | key="D" | <-- matched, reuse DOM node
+---------+ +---------+
| key="D" |
+---------+
Result: React removes B's DOM node, keeps A, C, D unchanged.
Minimum DOM operations.
Without keys (or with index keys on a reorderable list):
Old list: New list (item B removed):
+---------+ +---------+
| index=0 | "A" | index=0 | "A" <-- match, same content, OK
+---------+ +---------+
| index=1 | "B" | index=1 | "C" <-- match, content differs, UPDATE DOM
+---------+ +---------+
| index=2 | "C" | index=2 | "D" <-- match, content differs, UPDATE DOM
+---------+ +---------+
| index=3 | "D" <-- no match, REMOVE DOM node
+---------+
Result: React updates index 1 and 2 content, removes index 3.
More DOM operations, and component state gets mixed up.
The Bug: Lists Without Stable Keys
Here is a concrete example of what goes wrong when using index keys on a dynamic list.
import { useState } from 'react';
function InputItem({ defaultValue }) {
// Each item has its own local state (the input value)
const [value, setValue] = useState(defaultValue);
return (
<input value={value} onChange={e => setValue(e.target.value)} />
);
}
function BrokenList() {
const [items, setItems] = useState(['Apple', 'Banana', 'Cherry']);
const removeFirst = () => {
setItems(prev => prev.slice(1));
};
return (
<div>
<button onClick={removeFirst}>Remove first</button>
{items.map((item, index) => (
// BUG: using index as key
<InputItem key={index} defaultValue={item} />
))}
</div>
);
}
What happens when you click "Remove first":
- Before:
["Apple", "Banana", "Cherry"]with keys[0, 1, 2] - After:
["Banana", "Cherry"]with keys[0, 1] - React sees key
0still exists โ reuses the component instance that had "Apple" - React sees key
1still exists โ reuses the component instance that had "Banana" - React sees key
2is gone โ unmounts that instance - Result: The first input still shows "Apple" even though it should show "Banana"
The fix is to use stable, unique identifiers:
function FixedList() {
const [items, setItems] = useState([
{ id: 1, text: 'Apple' },
{ id: 2, text: 'Banana' },
{ id: 3, text: 'Cherry' },
]);
const removeFirst = () => {
setItems(prev => prev.slice(1));
};
return (
<div>
<button onClick={removeFirst}>Remove first</button>
{items.map(item => (
<InputItem key={item.id} defaultValue={item.text} />
))}
</div>
);
}
Now removing "Apple" (id: 1) means key 1 disappears. Keys 2 and 3 still match their previous instances, so "Banana" and "Cherry" keep their correct state.
Key Strategies: ID vs Index
Use Unique IDs (Preferred)
// From a database
{users.map(user => <UserCard key={user.id} user={user} />)}
// Generated with crypto
{items.map(item => <Item key={item.uuid} data={item} />)}
// From the data itself (if guaranteed unique)
{countries.map(country => <Option key={country.code} value={country.name} />)}
When Index Keys Are Safe
Index keys are acceptable only when all three conditions are true:
- The list is static (never reordered, filtered, or sorted)
- Items have no local state or uncontrolled inputs
- Items are never added or removed from the middle
// SAFE: static navigation links, no state, no reordering
const navLinks = ['Home', 'About', 'Contact'];
function Nav() {
return (
<nav>
{navLinks.map((link, index) => (
<a key={index} href={`/${link.toLowerCase()}`}>{link}</a>
))}
</nav>
);
}
Comparison Table
| Scenario | key= | key= |
|---|---|---|
| Items can be reordered | Correct | Buggy |
| Items can be removed from middle | Correct | Buggy |
| Items have local state (inputs) | Correct | State mismatch |
| Static, read-only list | Correct (overkill but safe) | Safe |
| Items added to beginning | Correct | Unnecessary re-renders |
| Performance on large lists | Optimal diffing | Extra DOM operations |
Keys and Component Identity
Keys do more than optimize lists. They control component identity. When a key changes, React treats it as a completely different component โ it unmounts the old one and mounts a new one.
Resetting State with Keys
function ProfilePage({ userId }) {
// BUG: If userId changes, the old comment draft persists
return (
<div>
<h1>User {userId}</h1>
<CommentForm />
</div>
);
}
// FIX: key forces remount, resetting all state inside CommentForm
function ProfilePage({ userId }) {
return (
<div>
<h1>User {userId}</h1>
<CommentForm key={userId} />
</div>
);
}
This is a deliberate technique, not a hack. React documents this as the recommended way to reset a component's state in response to a prop change.
userId changes from 1 to 2:
Without key:
CommentForm instance stays mounted
State persists (stale draft from user 1 visible for user 2)
With key={userId}:
CommentForm (key=1) unmounts -> state destroyed
CommentForm (key=2) mounts -> fresh state
Forcing Re-initialization
function Timer({ duration }) {
const [timeLeft, setTimeLeft] = useState(duration);
useEffect(() => {
const id = setInterval(() => {
setTimeLeft(t => {
if (t <= 0) { clearInterval(id); return 0; }
return t - 1;
});
}, 1000);
return () => clearInterval(id);
}, []);
return <p>{timeLeft}s remaining</p>;
}
// Usage: key resets the timer completely when duration changes
function Game({ round }) {
return <Timer key={round} duration={30} />;
}
Keys and Animations
Keys affect CSS transition and animation libraries. When a key changes, the old element unmounts and a new one mounts โ giving animation libraries the enter/exit signals they need.
import { AnimatePresence, motion } from 'framer-motion';
function Slideshow({ images, currentIndex }) {
return (
<AnimatePresence mode="wait">
<motion.img
key={images[currentIndex].id} // key change triggers exit + enter
src={images[currentIndex].src}
initial={{ opacity: 0, x: 100 }}
animate={{ opacity: 1, x: 0 }}
exit={{ opacity: 0, x: -100 }}
/>
</AnimatePresence>
);
}
Without the key, AnimatePresence cannot detect that the element changed โ it sees the same motion.img component and just updates props, skipping the exit animation entirely.
List Animations
function AnimatedList({ items }) {
return (
<AnimatePresence>
{items.map(item => (
<motion.div
key={item.id} // stable keys let AnimatePresence track items
initial={{ opacity: 0, height: 0 }}
animate={{ opacity: 1, height: 'auto' }}
exit={{ opacity: 0, height: 0 }}
layout // animate position changes on reorder
>
{item.text}
</motion.div>
))}
</AnimatePresence>
);
}
Performance Implications
Keys directly affect React's reconciliation efficiency. Poor key choices cause unnecessary DOM mutations.
Benchmark: Adding to Beginning of List
// Scenario: prepend new item to a list of 1000 items
// key={index}: React sees that EVERY key has different content
// Result: updates 1000 DOM text nodes (O(n) mutations)
// key={item.id}: React sees 999 existing keys unchanged, 1 new key
// Result: creates 1 new DOM node, inserts it (O(1) mutations)
Benchmark: Removing From Middle
// Scenario: remove item at position 500 from a list of 1000
// key={index}: items 500-999 all shift up by one position
// Result: updates 500 DOM nodes (O(n) mutations)
// key={item.id}: only the removed key disappears
// Result: removes 1 DOM node (O(1) mutations)
For small lists (under 50 items), the performance difference is negligible. For large lists, proper keys can mean the difference between a smooth 60fps interaction and visible jank.
Common Mistakes
Using Random Values as Keys
// TERRIBLE: new key every render, every item remounts every time
{items.map(item => (
<Item key={Math.random()} data={item} />
))}
This is worse than no key at all. Every render produces new keys, so React unmounts and remounts every component โ destroying state, losing focus, killing performance, and breaking animations.
Using Non-Unique Keys
// BUG: if two items share the same name, React treats them as the same element
{items.map(item => (
<Item key={item.name} data={item} /> // names might not be unique
))}
React warns in the console when it detects duplicate keys. Always use values you know are unique: database IDs, UUIDs, or guaranteed-unique natural keys.
Keys on Fragments
// Keys on Fragments use the explicit <Fragment> syntax
import { Fragment } from 'react';
{groups.map(group => (
<Fragment key={group.id}>
<h2>{group.title}</h2>
<p>{group.description}</p>
</Fragment>
))}
// Short syntax <> does not support keys
// This is a syntax error:
// {groups.map(group => (
// <> key={group.id}> // WRONG
// ))}
Generating Stable Keys
When your data does not have natural IDs, generate them at creation time โ never at render time.
import { useRef, useCallback } from 'react';
function useDynamicList(initialItems = []) {
const nextId = useRef(0);
const addItem = useCallback((text) => {
return { id: nextId.current++, text };
}, []);
// Initialize with stable IDs
const [items, setItems] = useState(() =>
initialItems.map(text => ({ id: nextId.current++, text }))
);
const add = useCallback((text) => {
setItems(prev => [...prev, addItem(text)]);
}, [addItem]);
const remove = useCallback((id) => {
setItems(prev => prev.filter(item => item.id !== id));
}, []);
return { items, add, remove };
}
Usage:
function TodoApp() {
const { items, add, remove } = useDynamicList(['Buy milk', 'Walk dog']);
const [text, setText] = useState('');
const handleAdd = () => {
if (text.trim()) {
add(text.trim());
setText('');
}
};
return (
<div>
<input value={text} onChange={e => setText(e.target.value)} />
<button onClick={handleAdd}>Add</button>
{items.map(item => (
<div key={item.id}>
<span>{item.text}</span>
<button onClick={() => remove(item.id)}>Remove</button>
</div>
))}
</div>
);
}
Quick Reference
ALWAYS use:
- Database IDs: key={user.id}
- UUIDs: key={item.uuid}
- Unique natural keys: key={country.code}
SOMETIMES OK:
- Index keys on STATIC, STATELESS, NEVER-REORDERED lists: key={index}
NEVER use:
- Math.random()
- Date.now()
- Any value generated during render
Key Takeaways
- Keys tell React which list items correspond between renders
- Without stable keys, React falls back to positional comparison โ causing state bugs and unnecessary DOM updates
- Use unique, stable identifiers (database IDs, UUIDs) as keys
- Index keys are safe only for static, stateless lists that never reorder
- Changing a key forces React to unmount and remount โ useful for resetting state
- Keys enable animation libraries to detect enter/exit transitions
- Never generate keys during render (Math.random, Date.now)
- For large lists, proper keys are a meaningful performance optimization
- Keys on
<Fragment>require the explicit import syntax, not<>