Curriculum Series

Why array indices make bad React keys

Why array indices make bad React keys

Why array indices make bad React keys

React

TL;DR

Using array indices as key props looks fine until the list is reordered, filtered, or has items removed. React uses keys to figure out which component corresponds to which item between renders — with indices, that mapping breaks the moment positions change, and you get stale state, wrong content, and visual glitches.

What keys are actually for

When React renders a list, it needs to match items between renders so it can update the DOM efficiently instead of rebuilding it from scratch.

javascript
// React uses keys to match items between renders
{
items.map((item) => (
<ListItem key={item.id} item={item} />
));
}

Between renders, React uses keys to decide:

  • Which items are new and need to be mounted
  • Which items are gone and need to be unmounted
  • Which items moved and need to be reordered

Where index keys fall apart

Consider:

javascript
{
items.map((item, index) => (
<ListItem key={index} item={item} />
));
}

Start with ["Apple", "Banana", "Cherry"] and delete "Apple":

Before deletion:

Index (key)Item0Apple1Banana2Cherry

After deletion:

Index (key)Item0Banana1Cherry

From React's point of view, key 0 still exists — but its props changed from "Apple" to "Banana". So React mutates the existing <ListItem> in place rather than mounting a fresh one. It then unmounts key 2 (the old "Cherry" instance), even though conceptually "Cherry" is still in the list. Everything lines up wrong.

The bug you actually ship

The issue becomes obvious the moment list items carry their own local state:

javascript
function TodoList() {
const [todos, setTodos] = useState([
'Buy milk',
'Walk dog',
'Read book',
]);
const removeTodo = (index) => {
setTodos(todos.filter((_, i) => i !== index));
};
return (
<ul>
{todos.map((todo, index) => (
<TodoItem
key={index}
text={todo}
onRemove={() => removeTodo(index)}
/>
))}
</ul>
);
}
function TodoItem({ text, onRemove }) {
const [checked, setChecked] = useState(false);
return (
<li>
<input
type="checkbox"
checked={checked}
onChange={() => setChecked(!checked)}
/>
{text}
<button onClick={onRemove}>Remove</button>
</li>
);
}

Check "Buy milk", then delete it. The checked state lives in the TodoItem at key 0. After deletion, key 0 is "Walk dog" — and it shows up checked, even though you never touched its box.

When indices are actually fine

You can get away with index keys when all three of these hold:

  1. The list never reorders, adds, or removes items.
  2. Items carry no local state (no form inputs, no checkboxes, no animations).
  3. There's no stable unique id on the data.

Example — a static nav:

javascript
{
navLinks.map((link, index) => (
<a key={index} href={link.url}>
{link.label}
</a>
));
}

What to use instead

Anything stable and unique per item:

javascript
// Use an ID from the data
{
users.map((user) => (
<UserCard key={user.id} user={user} />
));
}
// If the data has no ID, generate one when creating the item
const newTodo = {
id: crypto.randomUUID(),
text: 'New todo',
};

The key here is "generate once, when the item is created" — not per render.

Common Pitfalls

Never use Math.random() or Date.now() inline as a key. Those values change every render, so React treats every item as new every time, unmounting and remounting everything. That's strictly worse than index keys.

Interview Tip

The checkbox example is the most visceral way to demonstrate the bug — a check mark "jumps" to the wrong item. Then explain the fix (stable id) and, as a bonus, list the three conditions where index keys are legitimately OK. That trio shows you understand the rule rather than parroting it.

Why interviewers ask this

This question separates people who know the rule from people who understand reconciliation. A lot of candidates can recite "don't use index as key" without being able to say why or when it's fine. A senior-level answer covers the mechanism (how React matches keyed elements) and the exceptions.

Finished reading?

Mark this topic as solved to track your progress.