The React key Prop: Why Index Keys Break Your Lists
What React does with keys during reconciliation, the concrete bugs index keys cause, and how to pick a key when your data has no id.
Table of contents
- What React does with keys
- The bug, concretely
- Choosing a key when there is no id
- Using key deliberately to reset state
- Frequently asked questions
- Does the key prop get passed to my component?
- Why is my key warning appearing for a fragment?
- Does using better keys improve performance?
- Can I use JSON.stringify(item) as a key?
- Related reading
- References
key tells React which element in a list corresponds to which element in the previous render. Get it wrong and React reuses the wrong component instance — which produces bugs that look impossible.
What React does with keys#
When a list re-renders, React matches old children to new children by key. A matched key means "same component, update its props". A missing match means "unmount the old, mount the new".
Without a key, React falls back to position. With key={index}, you have explicitly told React that position is identity — which is only true if the list never reorders, never has insertions except at the end, and never has deletions.
The bug, concretely#
// Broken
{
todos.map((todo, i) => <TodoItem key={i} todo={todo} />);
}Start with three todos. Each TodoItem holds local state — say, whether its edit field is open.
key=0 "Buy milk" (editing)
key=1 "Walk dog"
key=2 "Write post"Delete "Buy milk". Now the array is ["Walk dog", "Write post"], so:
key=0 "Walk dog" ← still has the "editing" state from "Buy milk"
key=1 "Write post"React sees key 0 in both renders, decides it is the same component, and keeps its state — but the props now describe a different todo. The edit box is open on the wrong row. The same mechanism corrupts uncontrolled input values, animation state, and focus.
// Correct
{
todos.map((todo) => <TodoItem key={todo.id} todo={todo} />);
}Choosing a key when there is no id#
In order of preference:
- A stable id from your data. Database id, UUID, slug.
- A natural unique field. An email, a file path, an ISO timestamp — as long as it cannot repeat.
- A composite.
key={`${row}-${column}`}for a grid. - An id generated at creation time. If the list is created client-side, assign
crypto.randomUUID()when you add the item — not during render.
// Wrong: a new key every render means the component remounts every render
<Item key={crypto.randomUUID()} />;
// Right: the id is part of the data
setItems([...items, { id: crypto.randomUUID(), text }]);Index keys are acceptable in exactly one case: a static list that never reorders, never has items inserted or removed, and whose children hold no state. A hard-coded nav menu qualifies. Most other things do not.
Using key deliberately to reset state#
Because changing a key unmounts and remounts, you can use it as a reset button:
// Remount the form — and clear all its internal state — when the user changes
<UserForm key={userId} userId={userId} />This is the officially recommended way to reset a component's state on a prop change, and it is much simpler than an effect that clears every field.
Frequently asked questions#
Does the key prop get passed to my component?#
No. key is consumed by React and never appears in props. If you need the value inside, pass it again under a different name.
Why is my key warning appearing for a fragment?#
Fragments in a list need a key too, which requires the explicit form: <Fragment key={id}> rather than <>.
Does using better keys improve performance?#
Usually yes, as a side effect: correct keys let React move DOM nodes rather than recreating them. But correctness is the reason to fix them.
Can I use JSON.stringify(item) as a key?#
It technically works, but it is long, slow, and changes whenever any field changes — which remounts the component on every edit. Use an id.
Related reading#
- React Performance Optimization
- React Hooks Guide — where the retained state lives