Problem Solved

React · missing key

Home › Courses › React › Problems › Missing key
⚛️ Problem · 5 min read

"Each child should have a unique key."
The fix, and why index keys are wrong.

React renders a list with .map(), and the console warns about missing keys. Every beginner adds key={index} to silence it. Then a bug appears: deleting an item leaves the wrong state in the wrong row. This page explains why keys matter, why index keys break, and the stable-ID pattern that actually works.

Short answer: Add key={item.id} to the root element inside your .map(). Keys must be stable (same value for the same item across renders) and unique among siblings. Never use array indices if the list can reorder, insert, or delete. If your data has no ID, generate one with crypto.randomUUID() when the item is created — not during render.

✅ Solution 1 — The correct fix: stable key={item.id}

Unique ID from your data. Stable across renders. React tracks items correctly.

React uses the key prop to match elements between renders. When you add an item, React needs to know which element is new. When you delete one, React needs to know which one is gone. A stable key lets React answer those questions correctly — and only the changed element re-renders.

correct-key.tsx
import { useState } from 'react';

interface User {
id: string;
name: string;
email: string;
}

// ─── BAD: no key, or index as key ────────────
function BadList({ users }: { users: User[] }) {
return (
    {users.map((user, index) => (
  • {/* ⚠️ index key */} {user.name} — {user.email}
  • ))}
); } // ─── GOOD: stable ID as key ────────────────── function GoodList({ users }: { users: User[] }) { return (
    {users.map(user => (
  • {/* ✅ stable id */} {user.name} — {user.email}
  • ))}
); } // ─── Why index breaks — a live demo ────────── function IndexKeyBug() { const [users, setUsers] = useState([ { id: 'a', name: 'Alice', email: 'a@x.com' }, { id: 'b', name: 'Bob', email: 'b@x.com' }, { id: 'c', name: 'Carol', email: 'c@x.com' } ]); function remove(id: string) { setUsers(u => u.filter(x => x.id !== id)); } return (

With ID key (correct)

    {users.map(u => (
  • {u.name}
  • ))}
{/* If you switch to key={index}: - Delete Alice (index 0) - Bob moves to index 0 - React sees "the element at index 0 changed" - Any state inside that row (input, checkbox, focus) leaks from Alice's row to Bob's row - THIS is the bug index keys cause */}
); } console.log(BadList, GoodList, IndexKeyBug);
import { useState } from 'react';

function BadList({ users }) {
return React.createElement('ul', null,
users.map((user, index) =>
React.createElement('li', { key: index },
user.name, ' — ', user.email
)
)
);
}

function GoodList({ users }) {
return React.createElement('ul', null,
users.map(user =>
React.createElement('li', { key: user.id },
user.name, ' — ', user.email
)
)
);
}

function IndexKeyBug() {
const [users, setUsers] = useState([
{ id: 'a', name: 'Alice', email: 'a@x.com' },
{ id: 'b', name: 'Bob', email: 'b@x.com' },
{ id: 'c', name: 'Carol', email: 'c@x.com' }
]);

function remove(id) {
setUsers(u => u.filter(x => x.id !== id));
}

return React.createElement('div', null,
React.createElement('h3', null, 'With ID key (correct)'),
React.createElement('ul', null,
users.map(u =>
React.createElement('li', { key: u.id },
u.name,
React.createElement('button', { onClick: () => remove(u.id) }, '×')
)
)
)
);
}

console.log(BadList, GoodList, IndexKeyBug);

Key = identity

The key tells React which element is which across renders. Same key = same element = React preserves its state, DOM, focus.

Unique among siblings

Not globally unique — just within the array. Two lists can have the same keys. Only siblings compete for uniqueness.

Stable across renders

If the key changes, React treats it as a new element — remounts, loses state. Use a stable ID from your data, not Math.random().

Attach to the root

The key goes on the outermost element returned by .map(). Not on a nested child, not on a fragment wrapper.

Keys are the most misunderstood prop in React. They're not passed to the component as a prop — React intercepts them. They're not for you to use inside the component — they're for React's reconciliation algorithm. They exist for one reason: telling React "these are the same elements as last time, even if they're in a different order." Get this right and lists behave correctly forever. Get it wrong and you'll debug "why did my input lose focus" for hours.

✅ Solution 2 — Why index keys break

The bug that surfaces weeks after you ship.

Index keys seem harmless — until the list changes. When you delete an item, the item that was at index 2 becomes index 1, and React sees "the element at index 1 changed." Any state inside that element (input value, checkbox, focus, animation) stays with the index, not with the item. The result: state leaks between rows.

index-key-bug.tsx
import { useState } from 'react';

interface Row {
id: string;
label: string;
}

// ─── The bug, demonstrated ──────────────────
function IndexKeyBug() {
const [rows, setRows] = useState([
{ id: 'a', label: 'Row A' },
{ id: 'b', label: 'Row B' },
{ id: 'c', label: 'Row C' }
]);

function remove(id: string) {
setRows(r => r.filter(x => x.id !== id));
}

return (

Try this: type "AAA" in Row A's input, then delete Row A.

{/* ✅ CORRECT — key={row.id} */}

Correct: key={{row.id}}

{rows.map(row => ( remove(row.id)} /> ))} {/* ❌ BUGGY — key={{index}} */}

Buggy: key={{index}}

{rows.map((row, index) => ( remove(row.id)} /> ))}
); } function RowComponent({ label, onDelete }: { label: string; onDelete: () => void }) { // Local state that doesn't belong to the parent const [text, setText] = useState(''); return (
'flex', gap: 8, margin: '4px 0' }}> 80 }}>{label} setText(e.target.value)} placeholder="type here..." />
); } // ─── What happens with index keys ───────────── // // Initial state: // Row A (index 0): text = 'AAA' ← user typed this // Row B (index 1): text = '' // Row C (index 2): text = '' // // User deletes Row A. // New array: // Row B (index 0) // Row C (index 1) // // React sees: // "Index 0: was 'Row A', now 'Row B' — same element, update the label" // "Index 1: was 'Row B', now 'Row C' — same element, update the label" // "Index 2: was 'Row C', now gone — unmount" // // Result: // Row B's input now contains "AAA" (leaked from Row A) // Row C's input is empty (was empty) // // The state leaked from one item to another because // the key (index) stayed the same — React thinks // it's the same component. // // ───────────────────────────────────────────── // // With key={row.id}, React sees: // "key 'a' is gone — unmount" // "key 'b' moved — same element, keep its state" // "key 'c' moved — same element, keep its state" // // Result: // Row B's input is empty (correct) // Row C's input is empty (correct) // No state leaks. Correct. console.log(IndexKeyBug, RowComponent);
import { useState } from 'react';

function IndexKeyBug() {
const [rows, setRows] = useState([
{ id: 'a', label: 'Row A' },
{ id: 'b', label: 'Row B' },
{ id: 'c', label: 'Row C' }
]);

function remove(id) {
setRows(r => r.filter(x => x.id !== id));
}

return React.createElement('div', null,
React.createElement('p', null, 'Try this: type "AAA" in Row A's input, then delete Row A.'),
React.createElement('h3', null, 'Correct: key={row.id}'),
rows.map(row =>
React.createElement(RowComponent, {
key: row.id,
label: row.label,
onDelete: () => remove(row.id)
})
),
React.createElement('h3', null, 'Buggy: key={index}'),
rows.map((row, index) =>
React.createElement(RowComponent, {
key: index,
label: row.label,
onDelete: () => remove(row.id)
})
)
);
}

function RowComponent({ label, onDelete }) {
const [text, setText] = useState('');
return React.createElement('div', { style: { display: 'flex', gap: 8, margin: '4px 0' } },
React.createElement('span', { style: { width: 80 } }, label),
React.createElement('input', { value: text, onChange: e => setText(e.target.value), placeholder: 'type here...' }),
React.createElement('button', { onClick: onDelete }, '×')
);
}

console.log(IndexKeyBug, RowComponent);

State leaks between rows

Delete an item, and the next row's state (input value, checkbox) moves up. Index keys make React think "same element."

Focus jumps

If an input was focused, deleting a row above it can lose focus — React didn't track that element's identity.

Animations glitch

Reordering animations with index keys play in the wrong direction because React thinks the wrong element moved.

React 18 stricter

React 18+ warns on index keys more aggressively. It's not a bug — it's React telling you the truth about a real bug.

✅ Solution 3 — Generate IDs when data has none

For local lists — form rows, optimistic UI, temp items.

Not all data has IDs. Form rows, drag-and-drop lists, optimistic UI items — these are created locally and need IDs. Generate one when the item enters the list, not on every render. crypto.randomUUID() is the modern standard — stable, unique, no dependency.

generate-ids.tsx
import { useState } from 'react';

// ─── Form rows with generated IDs ────────────
interface FormRow {
id: string;
value: string;
}

function DynamicForm() {
const [rows, setRows] = useState([
{ id: crypto.randomUUID(), value: '' }
]);

function addRow() {
setRows(r => [
...r,
{ id: crypto.randomUUID(), value: '' } // ✅ new ID
]);
}

function removeRow(id: string) {
setRows(r => r.filter(x => x.id !== id));
}

function updateRow(id: string, value: string) {
setRows(r => r.map(x => x.id === id ? { ...x, value } : x));
}

return (
{rows.map(row => (
'flex', gap: 8 }}> updateRow(row.id, e.target.value)} />
))}
); } // ─── BAD: generate IDs during render ───────── function BadIds({ items }: { items: string[] }) { return (
    {items.map(item => ( {/* ❌ New ID on every render — React remounts every item */}
  • crypto.randomUUID()}>{item}
  • ))}
); } // ─── BAD: Math.random() as fallback ────────── function BadRandom({ items }: { items: { name: string }[] }) { return (
    {items.map(item => ( {/* ❌ Same problem — new key each render */}
  • Math.random()}>{item.name}
  • ))}
); } // ─── When to use what ──────────────────────── // // ✅ Data has an ID: // key={item.id} // // ✅ Local form rows / optimistic UI: // Generate crypto.randomUUID() when item is created // // ✅ Backend IDs available: // key={item.uuid || item.id || item.slug} // // ❌ Never: // key={index} — breaks on reorder/delete // key={Math.random()} — remounts on every render // key={crypto.randomUUID()} inside render — same as above console.log(DynamicForm, BadIds, BadRandom);
import { useState } from 'react';

function DynamicForm() {
const [rows, setRows] = useState([
{ id: crypto.randomUUID(), value: '' }
]);

function addRow() {
setRows(r => [...r, { id: crypto.randomUUID(), value: '' }]);
}
function removeRow(id) {
setRows(r => r.filter(x => x.id !== id));
}
function updateRow(id, value) {
setRows(r => r.map(x => x.id === id ? { ...x, value } : x));
}

return React.createElement('div', null,
rows.map(row =>
React.createElement('div', { key: row.id, style: { display: 'flex', gap: 8 } },
React.createElement('input', {
value: row.value,
onChange: e => updateRow(row.id, e.target.value)
}),
React.createElement('button', { onClick: () => removeRow(row.id) }, 'Remove')
)
),
React.createElement('button', { onClick: addRow }, '+ Add row')
);
}

function BadIds({ items }) {
return React.createElement('ul', null,
items.map(item =>
React.createElement('li', { key: crypto.randomUUID() }, item)
)
);
}

function BadRandom({ items }) {
return React.createElement('ul', null,
items.map(item =>
React.createElement('li', { key: Math.random() }, item.name)
)
);
}

console.log(DynamicForm, BadIds, BadRandom);

Generate when created

crypto.randomUUID() when the item enters the list. The ID then lives in state — stable across renders.

Never during render

key={crypto.randomUUID()} inside .map() produces a new key every render — full remount. Worse than the bug.

Server IDs are best

If the API returns an id, uuid, or slug, use it. It's stable, unique, and no extra work.

Fallback chain

item.id ?? item.uuid ?? item.slug for polymorphic data. All are stable. Just pick whichever the item has.

crypto.randomUUID() inside render is worse than index keys. Index keys cause state leaks on reorder; random keys force React to unmount and remount every item every render — full DOM destruction, lost focus, lost state, no animations. The fix for a missing key is a stable ID. If you can't produce a stable ID, use the index as a temporary measure — but plan to add IDs. Never use random values as keys during render.

⚠️ Common mistakes

The key mistakes that surface weeks later.

1. Using index as key

Works until items reorder or delete. Then state leaks. Fix: use item.id or a stable identifier.

2. Using Math.random()

New key every render — full remount. Fix: stable ID or generate once when the item is created.

3. Not putting the key on the root

  • — warning. Fix:
  • .

    4. Using key as a prop

    key isn't passed to the component. If you need it inside, pass it separately. Fix: key={id} id={id}.

    5. Keys duplicated across siblings

    key={item.category} when two items share a category — duplicate key warning. Fix: unique per item.

    6. Assuming keys need to be global

    Two separate lists can use the same key. Fix: only unique within the array. Don't overcomplicate with prefixes.

    7. Key on a fragment in a map

    ... works, but <>... can't take a key. Fix: use React.Fragment key={...}.

    8. Silencing the warning with suppressHydrationWarning

    Doesn't help — that's for SSR mismatches. Fix: add the actual key.

    ❓ Frequently asked

    What does "Each child in a list should have a unique key" mean?

    When you render an array of elements with .map(), each returned element needs a key prop that is unique among its siblings. React uses keys to identify which items changed, were added, or were removed between renders. Without keys, React falls back to index-based reconciliation, which causes bugs when items reorder, insert, or delete.

    Why not use index as the key?

    Index keys work only if the list never reorders, inserts, or deletes. When it does, React reuses the wrong component state — inputs keep wrong values, animations jump, local state leaks between items. Use a stable ID from your data (item.id, item.uuid, item.slug).

    Do keys need to be globally unique?

    No — keys only need to be unique among siblings in the same array. Two separate .map() calls can use the same key values without conflict. The uniqueness scope is the immediate sibling set.

    What if my data has no ID?

    Generate one when the item enters the list, not on every render. If items come from an API, use whatever the server provides. If items are purely local, generate ids with crypto.randomUUID() when creating the item. Never generate a key with Math.random() inside render.

    🔗 Related

    ← All React problems
    Problem solved · React