Errors and fixes
React key warning: why it appears and when the index is wrong
React asks for a stable key on every list item. The index works only when the list is never reordered, filtered or extended.
Key points
- The key must identify the element, not its position: use the record id.
- The index is fine only for static lists that never change order.
- A duplicate key raises no error but shares state between two rows.
Short answer: React needs a stable key to recognise an element between two renders. Use the record id. The index is fine only for a static list that never changes order.
The exact message
Warning: Each child in a list should have a unique "key" prop.
Check the render method of `OrderList`. See
https://react.dev/link/warning-keys for more information.
Why a key and not an index
| List | Index | Stable id |
|---|---|---|
| static, never reordered | acceptable | preferable |
| filterable | state lands on the wrong row | correct |
| reorderable | inconsistent inputs and checkboxes | correct |
| with items removed | state that migrates | correct |
React does not use the key for the DOM. It uses it to decide which component instance to keep. With the index, row 2 stays row 2 even when the record changes, so local state (typed text, checkbox, focus) stays tied to the position.
The mechanism: the key decides which instance survives
The reconciler compares the children of an element by position and by key. When the key changes React does not update the instance, it unmounts it and builds a new one. Unmounting means losing local state, re-running effects and replacing the DOM node. That is why an unstable key is not a style detail: it is the difference between updating one row and rebuilding the whole list on every render. The warning you saw appears in development only; in production the defect stays and prints nothing.
A diagnostic step for duplicate keys
The React warning covers a missing key, not a duplicate one: two records sharing an id produce silence. The guard below costs one function and belongs in development, not in the production bundle: it fails on the first ambiguous value and tells you which one repeats. If you run it and it passes, the problem is not the dataset but the layer that transforms it: a map that rewrites the id, a concatenation with an index, a key built from mutable fields.
// solo in sviluppo: fallisce subito su una key duplicata
function assertUniqueKeys(rows: T[], keyOf: (row: T) => string) {
const seen = new Set();
for (const row of rows) {
const key = keyOf(row);
if (seen.has(key)) throw new Error(`key duplicata: ${key}`);
seen.add(key);
}
return rows;
}
The fix
type Order = { id: string; total: number };
export function OrderList({ orders }: { orders: Order[] }) {
return (
{orders.map((order) => (
{order.total} €
))}
);
}
If the record has no id, generate a stable one when you create it, not during the render. crypto.randomUUID() inside the component produces a new key on every render and React recreates everything: worse than the index.
Fragments and components
import { Fragment } from "react";
{rows.map((row) => (
{row.label}
{row.value}
))}
The short syntax <>...</> accepts no attributes, so you need the explicit form. The same applies to a component that returns several elements.
When the fix does not work
If the warning stays, the key is missing one level down: a wrapper inside the map that returns an array without keys. If instead you see odd behaviour and no warning, look for duplicate keys. Two records with the same id raise no error, but React reuses the same instance for both.
When the common fix makes things worse
The instinctive fix replaces the index with something unique generated at render time: key={crypto.randomUUID()} or key={JSON.stringify(row)}. The first changes on every render and remounts every row, so typed text disappears, focus is lost and transitions restart. The second looks stable but depends on the whole content: every character typed into an editable row changes its shape and remounts that row while the user writes. In both cases the warning disappears and the behaviour degrades. A key must be stable across renders and unique among siblings, and only the record id meets both conditions.
A limitation with a name: non-unique ids
If the dataset holds two records with the same id, no key separates them. It happens with aggregated data: two rows coming from the same order, a list merging results from two searches, a join without distinct. React cannot invent uniqueness the data does not have, and the symptom is one row inheriting the state of another. That is the name of the limitation: the key inherits the defects of its source. The correction belongs upstream, in the identifier a record is born with, not in the markup.
How to prevent it
- In the data model, give every record a stable id before it reaches the component.
- Never use
Math.random()orDate.now()as a key. - In tests, reorder the list and check that typed values follow the record, not the position.
An ESLint rule closes the missing-key case, not the wrong-key one. For the second you need a test that reorders the list and checks that typed values follow the record: it is the only way to catch a key that compiles, warns about nothing and gets it wrong.
Where to go next
In the catalogue: data table with a preview, components and blocks. The guides: accessibility checklist and alternatives to MUI; the comparison: Radix, Base UI and React Aria.
Frequently asked questions
Why doesn't React use the index by default?
Because the index changes when the list changes: React would reuse the wrong state on another row, producing inconsistent inputs and checkboxes.
Is the key visible in the DOM?
No. The key is an instruction for the reconciler and is never written as an HTML attribute.
How do I give a Fragment a key?
Use the explicit form <Fragment key={row.id}>: the short <> syntax accepts no attributes.
What do two elements with the same key cause?
React warns in development and may reuse state in the wrong place. The usual symptom is an input that keeps the previous row's value.
Do static lists need a key?
Yes, React asks for one anyway. On a truly static list the index is acceptable, but a semantic key stays clearer.