React key prop warning (tested)
Warning: Each child in a list should have a unique "key" prop. is a development-only console warning React prints when you render an array of elements with .map() and forget the key prop. We reproduced the exact text, proved that an array index as key removes the warning without removing the underlying risk, and measured the stack trace React gives you to find the culprit.
1. The exact warning, reproduced
We rendered this with no key at all, using react-dom/server's renderToStaticMarkup under NODE_ENV=development:
function App() {
const items = ['a', 'b', 'c'];
return React.createElement('ul', null, items.map(item =>
React.createElement('li', null, item)
));
}
React printed this to console.error, verbatim (newlines preserved):
Warning: Each child in a list should have a unique "key" prop.
Check the top-level render call using <ul>. See https://reactjs.org/link/warning-keys for more information.
at li
at App
The second line names where to look. When the list lives inside a child component instead of at the top level, we measured that the message names that component instead. Rendering the same list inside a component called Child, itself rendered by Parent, gave:
Check the top-level render call using <div>.
at span
at Child (file:///tmp/reactkeytest/repro2.mjs:24:17)
at Parent
The stack trace is the fastest way to find the offending .map(): read the component names under "at", not just the warning's own wording.
2. Index-as-key: no runtime warning, but not actually fixed
We re-ran the same component, this time passing the array index as key:
items.map((item, i) =>
React.createElement('li', { key: i }, item)
)
Result: zero console.error calls. We captured console.error itself during the render and counted 0 invocations. This confirms a common point of confusion precisely: React's runtime warning only checks that a key prop is present and, at dev speed, flags obviously-duplicated keys — it does not know whether your keys are stable across re-renders, so key={"index"} satisfies it completely.
Separately, per the eslint-plugin-react docs (not measured on this box), the lint rule react/no-array-index-key flags key={i} on .map() output as a best-practice warning at lint time. That is a static-analysis opinion about index keys specifically, not the same check as React's runtime warning above: you can have one without the other in either direction (a stable non-index key passes both; a missing key fails only the runtime one; an index key fails only the lint rule).
3. Why index keys still cause bugs (explained, not measured in this guide)
React uses key to match array items across renders so it can keep each item's DOM node and internal state attached to the right data, instead of re-creating everything. An index is really "position", not "identity". If the list can be reordered, have items inserted/removed anywhere but the end, or be filtered, the index of a given logical item changes even though the item itself didn't — so React pairs each position's old state with a different item than before.
Concretely: a list of todo rows, each with its own <input> holding draft text, keyed by index. Delete row 0. Every remaining row shifts up one index. React sees "index 0 still exists, index 1 still exists, ..." and assumes those are the same components, so it keeps each <input>'s DOM node and its live text — just now attached to the wrong row's data. The result is draft text (or focus, or scroll position, or an uncontrolled input's value) appearing to "jump" to the wrong row after a delete, insert at the start/middle, or sort. A stable key (e.g. a database id or UUID) makes React move the right DOM node with the right item instead of reusing a slot by position.
4. The fix
- Use a stable, unique identifier from the data itself:
key={item.id}, not the array index. - If your data truly has no id, generate one when the item is created (e.g.
crypto.randomUUID()) and store it with the item — don't compute it from position during render. - Index keys are fine only for lists that are always static: never reordered, never filtered, never spliced mid-list, and never conditionally rendered. That is rare enough that it is safer to default to a real id.
- If the warning points at a component you don't own (e.g. inside a library), check that you're not spreading props over an array of children without a key at the call site.
If React is throwing a numbered production error instead of printing this warning, see Minified React error #130. For a different list/key confusion that only shows up with server rendering, see React hydration error in Next.js.
Frequently asked questions
What is the exact React key warning text?
We measured: 'Warning: Each child in a list should have a unique "key" prop.' followed by a blank line, 'Check the top-level render call using <tag>. See https://reactjs.org/link/warning-keys for more information.', then a stack trace naming the components involved.
Does using the array index as the key remove the warning?
Yes, measured: React's runtime console warning does not fire at all when you pass the array index as key. It only checks that some key is present, not that it is stable identity.
If the warning is gone, is my list safe?
Not necessarily. The warning being gone just means a key prop exists. An index key still breaks state association when the list is reordered, filtered, or has items inserted/removed anywhere but the end.
Is index-as-key also an ESLint error?
Per the eslint-plugin-react docs, the separate rule react/no-array-index-key flags index keys at lint time as a best-practice warning. That is independent of React's own runtime console warning covered above.
What should I use as a key instead of the index?
A stable id from your data, such as a database primary key or a UUID generated once when the item is created. Avoid deriving the key from the item's position in the array.
Written with AI assistance. Measured on 2026-10-03 with Node.js 24.21.0, React 18 and react-dom 18 (server) via react-dom/server renderToStaticMarkup, NODE_ENV=development, calling React.createElement directly (no JSX/Babel step). ESLint behaviour is per the eslint-plugin-react docs, not measured. PlainHeaders is our extension; React, react-dom and Node.js are free and open source.