Why index as key is often wrong
Using the array index as a key value seems to work fine, until a list gets sorted, filtered, or modified. Then React recycles internal component state between the wrong elements, sometimes with serious, hard-to-trace consequences.
Table of Contents
- 1. What the key prop actually does
- 2. Index as key: the obvious but deceptive solution
- 3. Concrete bug example: form fields in a todo list
- 4. The fix: stable, content-anchored IDs as key
- 5. A second example: animations and CSS transitions
- 6. The exception: static, never-changing lists
- 7. A common follow-up mistake: regenerating the UUID on every render
- 8. Debugging strategy: React DevTools and recognizing symptoms
- 9. Practical summary for everyday use
- 10. Summary
- 11. FAQ
1. What the key prop actually does
React uses the key prop to uniquely identify elements in a list across multiple renders. When comparing the previous virtual DOM tree with the new one, a process called reconciliation, React matches each element in the new tree to an element in the old tree based on its key value. If the key matches, React assumes it is the same logical instance and keeps that instance's internal state and DOM node instead of recreating them.
Without stable key values, React cannot reliably identify elements across renders and falls back to position-based heuristics, which quickly lead to incorrect matches. The key is therefore not merely a performance hint, it is an identity marker that directly determines which component instance keeps which state.
2. Index as key: the obvious but deceptive solution
When a list has no obvious unique ID, many developers intuitively reach for the array index as the key, since it is always available and guaranteed unique within the current render. As long as the order and composition of the list never change, this actually works without issue, because index and logical identity happen to coincide in that case.
The problem arises as soon as the list changes: inserting an element at the start, deleting one, or swapping the order shifts the index of every subsequent element, even though the actual logical identity of those elements stayed the same. React then sees a 'different' element at index 0 than before, even though the same element with a shifted position is sitting there, and misattributes React-internal state accordingly.
3. Concrete bug example: form fields in a todo list
A particularly illustrative example is a todo list where each item contains its own uncontrolled input field for notes. If you delete the first item from the list while using the index as key, React incorrectly keeps the DOM node and internal state at index 0, because React believes it is still the same component. The typed note text stays in the input field, but now belongs, in content, to the wrong todo entry.
This effect is so treacherous because it does not show up as a crash or an error message, but as a silent, semantically wrong data display. Users often notice only late that a note is suddenly attached to the wrong entry, and the bug is hard to trace without understanding the reconciliation mechanism.
function TodoList({ todos, onDelete }) {
return (
<ul>
{todos.map((todo, index) => (
// WRONG: index as key -- note state gets attached
// to the wrong todo after deletion
<li key={index}>
<span>{todo.title}</span>
<input type="text" placeholder="Note..." />
<button onClick={() => onDelete(todo.id)}>Delete</button>
</li>
))}
</ul>
);
}
4. The fix: stable, content-anchored IDs as key
The correct approach is to use a stable, content-anchored ID, typically a database ID or a UUID generated once when the element is created. This ID stays unchanged for the entire lifetime of the logical element, regardless of its current position in the list, and lets React correctly identify elements even after reordering, filtering, or deletion.
With a stable ID as key, the note text in the todo example above correctly stays attached to the original todo entry, even when a different item is removed from the list. React reliably identifies, based on the ID, which component instance was deleted and which one continues to exist unchanged at its new position.
function TodoList({ todos, onDelete }) {
return (
<ul>
{todos.map((todo) => (
// CORRECT: stable todo.id as key
<li key={todo.id}>
<span>{todo.title}</span>
<input type="text" placeholder="Note..." />
<button onClick={() => onDelete(todo.id)}>Delete</button>
</li>
))}
</ul>
);
}
5. A second example: animations and CSS transitions
Similar problems occur with lists paired with animation libraries that react to mount and unmount events of individual elements. If a sortable list uses the index as key, an animation library will not detect actual movement of an element during reordering, only a content change at the same position, because from React's point of view the same component instance formally remains at index 0.
The result is that elements do not visibly animate from position A to position B during reordering, instead only their text content swaps in place, which confuses users and completely defeats the purpose of the animation. With a stable ID as key, React instead recognizes that an existing component instance merely moved to a different position in the tree, allowing animation libraries to render the transition correctly.
6. The exception: static, never-changing lists
Index as key is not problematic in every case. When a list is guaranteed to be static, meaning it is never sorted, filtered, inserted into, or deleted from, and the elements themselves carry no internal state, for example plain display text without form fields, the index as key is safe, because the mapping between index and logical identity never changes.
One example would be a fixed list of month names or navigation entries that stays constant at runtime. In such cases, the effort of introducing artificial IDs is unnecessary, since no realistic scenario exists where the position-to-index mapping breaks. But as soon as even the theoretical possibility of reordering or filtering exists, a stable ID should be used from the start to avoid hard-to-find bugs later.
7. A common follow-up mistake: regenerating the UUID on every render
A related mistake occurs when developers try to avoid the index problem but regenerate the ID directly inside the render call with crypto.randomUUID(), instead of setting it once when the data is created. This changes the key value on every render, React treats every element as completely new, and remounts the entire list on every update, losing all internal state and hurting performance.
The correct place to generate the ID is at the moment the data object is created, for example when adding a new todo entry or when loading data from an API, never during rendering itself. If the ID comes from a backend, a stable database ID is usually already available and can be used directly as the key.
// WRONG: new UUID on every render
function TodoList({ todos }) {
return todos.map((todo) => (
<TodoItem key={crypto.randomUUID()} todo={todo} />
));
}
// CORRECT: set the ID once when the record is created
function addTodo(title) {
return { id: crypto.randomUUID(), title, done: false };
}
8. Debugging strategy: React DevTools and recognizing symptoms
Typical symptoms of incorrect key values include input fields showing the wrong content after deleting or sorting a list item, checkbox or toggle states sticking to the wrong row, or animations that do not trigger as expected during reordering. React also logs a warning in the browser console when list elements are rendered with no key prop at all, but not when a technically present, unsuitable index is used as the key.
In React DevTools, this behavior can be traced deliberately by inspecting a list element, deleting or reordering it in the UI, and observing whether the DOM nodes are actually recreated or merely moved. If a DOM node persists despite the logical element being deleted, that is a clear sign of a faulty key.
9. Practical summary for everyday use
The general recommendation is to default to a stable, content-anchored ID whenever list elements can carry their own state or could theoretically be reordered, filtered, inserted, or deleted. Only for demonstrably static, stateless lists is the index a legitimate, simple alternative.
The table below summarizes the key scenarios and shows which key strategy is appropriate for each, so the decision does not have to be rethought from scratch every time in day-to-day work.
| Scenario | Index as key | Stable ID as key | Recommendation |
|---|---|---|---|
| Static list without internal state | safe | also possible | index is sufficient |
| List with form fields per element | state recycling bugs | correct | always stable ID |
| List with delete/insert | wrong attribution | correct | always stable ID |
| List with sort animation | animation does not trigger | correct | always stable ID |
Mironsoft
React architecture, performance, and Magento frontend integration
React frontends that stay fast instead of slowing down with every feature?
We review existing React applications for unnecessary re-renders, bloated bundles, and fragile state management, then build a frontend that stays performant and connects cleanly to Magento or other backends.
Performance Audit
Systematically measuring and fixing re-renders, bundle size, and load times.
State Architecture
Cleanly separating context, client state, and server state instead of mixing everything.
Magento Integration
Building robust, type-safe GraphQL or REST integration with Magento.
10. Summary
key Prop: The Key Facts at a Glance
Core problem
Index as key ties component identity to position instead of logical identity.
Typical bug
State such as input field content stays attached to the wrong element after deleting or sorting.
Correct fix
Set a stable, content-anchored ID when the data record is created.
Allowed exception
Guaranteed static lists without internal state per element.