Keys, Identity, and Reset Patterns
The Key That Unlocks React Behavior
Every confusing React bug you've ever encountered -- state appearing on the wrong list item, inputs losing focus, animations replaying when they shouldn't -- traces back to one thing: how React identifies elements across renders. Keys are the mechanism, and here's the thing: most developers only understand half of it.
// This has a bug. Can you see it?
function ChatList({ chats }) {
return chats.map((chat, index) => (
<ChatPreview
key={index} // The bug is right here
chat={chat}
unreadCount={chat.unread}
/>
));
}
When a new message pushes a chat to the top of the list, every ChatPreview after it receives the wrong chat's state. Animations replay. Unread badges show wrong counts. The user sees a different chat's draft message in their input.
The Mental Model
Think of keys as name tags at a conference. Without name tags, the conference organizer seats people by the order they arrive — person in seat 1 is "the first person," person in seat 2 is "the second person." If people swap seats, the organizer assumes the person in seat 1 changed their name and updates their badge.
With name tags (keys), the organizer tracks people by name. Alice is Alice regardless of which seat she's in. If Alice moves from seat 1 to seat 3, the organizer moves her name plate — they don't give seat 3 a new badge with Alice's name while leaving a confused stranger in seat 1 with Alice's old materials.
Keys are name tags. Without them, React tracks by position. With them, React tracks by identity.
How Keys Work in Reconciliation
During reconciliation, React matches old fibers to new elements. For lists, the matching strategy is:
- No keys (or all keys are index): Match by position. Element at index 0 maps to old fiber at index 0.
- With keys: Build a map of
key → old fiber. Match new elements by looking up their key in the map.
// Without keys — position-based matching
// Old: [Alice, Bob, Charlie]
// New: [Bob, Charlie] (Alice removed)
// React's perspective:
// Position 0: Alice → Bob (update Alice's fiber with Bob's props)
// Position 1: Bob → Charlie (update Bob's fiber with Charlie's props)
// Position 2: Charlie → nothing (delete Charlie's fiber)
// Result: 2 updates + 1 deletion = 3 DOM operations
// BUG: Alice's state (draft message, scroll position) now shows on Bob's item
// With keys — identity-based matching
// Old: [Alice(key=1), Bob(key=2), Charlie(key=3)]
// New: [Bob(key=2), Charlie(key=3)]
// React's perspective:
// key=1 (Alice): not in new list → delete
// key=2 (Bob): in both → update props (none changed → skip)
// key=3 (Charlie): in both → update props (none changed → skip)
// Result: 1 deletion. Bob and Charlie keep their state.
The Index-as-Key Bug in Detail
Here's a concrete demonstration of the index-as-key problem:
function TodoList() {
const [todos, setTodos] = useState([
{ id: 1, text: 'Buy milk' },
{ id: 2, text: 'Walk dog' },
{ id: 3, text: 'Code' },
]);
function removeFirst() {
setTodos(todos.slice(1)); // Remove "Buy milk"
}
return (
<>
<button onClick={removeFirst}>Remove First</button>
{todos.map((todo, index) => (
<TodoItem key={index} todo={todo} />
))}
</>
);
}
function TodoItem({ todo }) {
const [checked, setChecked] = useState(false);
return (
<label>
<input type="checkbox" checked={checked} onChange={() => setChecked(!checked)} />
{todo.text}
</label>
);
}
Scenario: User checks "Buy milk" (index 0). Then clicks "Remove First."
With key={index}:
- Old:
[{key:0, "Buy milk" ✓}, {key:1, "Walk dog"}, {key:2, "Code"}] - New:
[{key:0, "Walk dog"}, {key:1, "Code"}] - React matches key 0 → key 0: updates text from "Buy milk" to "Walk dog" but keeps the checked state
- Result: "Walk dog" appears checked. The user never checked it.
With key={todo.id}:
- Old:
[{key:1, "Buy milk" ✓}, {key:2, "Walk dog"}, {key:3, "Code"}] - New:
[{key:2, "Walk dog"}, {key:3, "Code"}] - React: key 1 deleted, key 2 preserved (unchecked), key 3 preserved (unchecked)
- Result: Correct. "Walk dog" is unchecked.
Key-as-Reset: Intentional Remounting
Here's where keys get really interesting. They aren't just for lists. You can use them on any component to force React to destroy and recreate it:
function UserProfile({ userId }) {
// When userId changes, the entire form resets
return <ProfileForm key={userId} userId={userId} />;
}
function ProfileForm({ userId }) {
const [name, setName] = useState('');
const [email, setEmail] = useState('');
// State starts fresh for each userId because the key changed
// React unmounts the old ProfileForm and mounts a new one
useEffect(() => {
fetchUser(userId).then(user => {
setName(user.name);
setEmail(user.email);
});
}, [userId]);
return (
<form>
<input value={name} onChange={e => setName(e.target.value)} />
<input value={email} onChange={e => setEmail(e.target.value)} />
</form>
);
}
Without key={userId}, switching from user 1 to user 2 would keep the form mounted. The old user's name and email would flash briefly before the effect fetches new data. With the key, React unmounts the old form entirely and mounts a fresh one — no stale data flash.
Key-as-reset is the idiomatic way to "reset a component." Don't use useEffect to reset state when a prop changes — that causes an extra render with stale state. A key change is cleaner: one unmount, one fresh mount.
Production Scenario: The Leaking Chat Input
A chat application has a message input per conversation:
function ChatWindow({ conversationId }) {
return (
<div>
<MessageList conversationId={conversationId} />
<MessageInput conversationId={conversationId} />
</div>
);
}
function MessageInput({ conversationId }) {
const [draft, setDraft] = useState('');
return (
<input
value={draft}
onChange={e => setDraft(e.target.value)}
placeholder="Type a message..."
/>
);
}
Bug: User types "Hey Alice" in conversation with Alice. Switches to Bob's conversation. The input still shows "Hey Alice." The draft leaked between conversations because MessageInput was never unmounted — React saw the same component type at the same position and reused it.
Fix: Add a key to reset the input per conversation:
function ChatWindow({ conversationId }) {
return (
<div>
<MessageList conversationId={conversationId} />
<MessageInput key={conversationId} conversationId={conversationId} />
</div>
);
}
Now switching conversations unmounts the old MessageInput and mounts a fresh one with empty draft state.
Key-as-reset destroys all state in the component subtree, including child components. If your form has deeply nested components with their own state, all of it resets. This is usually what you want, but be aware — if you need to preserve some state across resets, lift it above the keyed component.
When Index-as-Key is Safe
Before you go adding IDs to everything, let's be fair: index-as-key is not always wrong. It's safe when ALL of these are true:
- The list is static — items are never reordered, inserted, or deleted
- Items have no state — no inputs, checkboxes, or component state
- Items are not animated — no transition state tied to item identity
// Safe: static navigation items, no state, no reordering
const navItems = ['Home', 'About', 'Contact'];
navItems.map((item, i) => <NavLink key={i}>{item}</NavLink>);
// Safe: static table headers
const headers = ['Name', 'Email', 'Role'];
headers.map((h, i) => <th key={i}>{h}</th>);
// NOT safe: any list where items can change
const [todos, setTodos] = useState(initialTodos);
todos.map((todo, i) => <TodoItem key={i} todo={todo} />); // Bug waiting to happen
If in doubt, use a stable unique identifier. The cost of a proper key is negligible, and the bugs from index-as-key are subtle and painful to debug.
Common Mistakes
-
Wrong: Using index as key for any list that changes (sort, filter, add, remove) Right: Use a stable unique ID from your data (database ID, UUID, or a computed stable hash)
-
Wrong: Generating random keys on each render: key=
{Math.random()}Right: Keys must be stable across renders. Generate IDs when data is created, not when it renders -
Wrong: Using useEffect to reset state when a prop changes Right: Use
key={prop}on the component to force a clean remount -
Wrong: Omitting keys entirely on list items Right: Always provide keys on mapped elements. React warns in development for a reason
Challenge
Fix the key bug
Quiz
Key Rules
- 1Keys control component identity during reconciliation. Same key + same type = same instance (state preserved). Different key = new instance (state reset).
- 2Never use array index as key for lists that reorder, insert, or delete. State and uncontrolled inputs will transfer to wrong items.
- 3Use
key={value}on non-list components to force a clean remount when value changes. This is the 'key as reset' pattern. - 4Keys must be stable (same across renders), unique (among siblings), and derived from data (not generated during render).
- 5Index-as-key is safe only for static, stateless, non-animated lists. When in doubt, use a proper ID.
- 6Key-as-reset is cleaner than useEffect-based state resets: no flash of stale state, no extra render cycle.