Fix: Do Not Use Array Index in Keys (React)
Why key={index} breaks on reorder and removal, how to prove it with an uncontrolled input, and the three ways to get a real key when your data has no id.
TL;DR#
/app/bad-index-key.js
1:54 error Do not use Array index in keys react/no-array-index-key
✖ 1 problem (1 error, 0 warnings)ESLint's react/no-array-index-key rejects key={index} in a .map() because the index is not a stable identity for the item — insert, delete or reorder the array and React starts matching the wrong DOM node to the wrong item. The fix is a key derived from the data itself: a database id, a slug, or any field that is unique and does not change when the list is edited. Jump to the fix if you already know why.
Why it happens#
const list = children.map((child, index) => <li key={index}>{child}</li>);React's own documentation is explicit about this exact pattern:
You might be tempted to use an item's index in the array as its key. In fact, that's what React will use if you don't specify a key at all. But the order in which you render items will change over time if an item is inserted, deleted, or if the array gets reordered. Index as a key often leads to subtle and confusing bugs.
key is how React's reconciler decides whether a DOM node represents the same logical item across two renders, so it can keep that node (and whatever state it holds) instead of tearing it down and rebuilding it. An index is not an identity — it is a position. Two renders can have the same index pointing at two completely different items.
The accepted answer on the Stack Overflow thread (97 votes on the question, 108 on this answer) states the consequence plainly: "Suppose the parent component gets an array of 10 items and renders 10 components based on the array. Suppose the 5th item is then removed from the array. On the next render the parent will receive an array of 9 items and so React will render 9 components. This will show up as the 10th component getting removed, instead of the 5th, because React has no way of differentiating between the items based on index."
That bug is invisible for a plain, read-only list — text just reflows and nobody notices the DOM nodes were shuffled. It becomes visible the moment a list item owns its own state: an uncontrolled <input>, a timer, an open/closed toggle, a CSS transition in flight. The DOM node for a given key is reused across renders, so whatever state lives inside that node travels to whatever item now has the same key — not to the item it used to represent.
Proving it#
A list of three uncontrolled text inputs, one per item, keyed by index. Typing into the first input and then deleting the first item should, if the bug is real, leave the typed text attached to the second item's input instead of disappearing with the row it was typed into.
Reproduced in a headless DOM (jsdom, no browser) with React 18.3.1 and react-dom 18.3.1, legacy ReactDOM.render/unmountComponentAtNode, 16 October 2026:
keyMode=index -> remaining input values: ["hello",""]
keyMode=id -> remaining input values: ["",""]
BUG REPRODUCED (index key leaks "hello" onto item b): true
NEGATIVE CONTROL (id key does NOT leak, stays empty): trueWith key={index}, after removing the first item, the first remaining <input> still holds "hello" — text that was typed into a row that no longer exists, now shown on a different item's input. The negative control is the same scenario with key={item.id}: the remaining input is empty, because React correctly unmounted the removed item's own DOM node instead of reusing it.
Fix#
Replace the index with something that uniquely and permanently identifies the item.
-
Use an id the data already has. Most API responses and database rows already carry one.
jsxconst list = children.map((child) => <li key={child.id}>{child.label}</li>); -
If the data has no id, add one when it is created — not during render.
key={Math.random()}or any key generated inside the render function regenerates on every render, which defeats the purpose of having a key at all. React's documentation calls this out directly: "do not generate keys on the fly… this will cause keys to never match up between renders, leading to all your components and DOM being recreated every time." Assign a stable id once, when the array is built (a counter,crypto.randomUUID(), or a library likeuuid), and keep that id attached to the object from then on. -
If no field is unique on its own, compose one. React's own rule is narrower than it sounds: "Keys must be unique among siblings" — not globally. A
nameconcatenated with arecipeIdonly needs to be unique within that one list; the same key can reappear in a different array rendered elsewhere without conflict.
Prefixing the index does not fix anything and still trips the rule:
/app/bad-index-key-prefixed.js
1:54 error Do not use Array index in keys react/no-array-index-keykey={`item-${index}`} is still derived from position, not identity — the string changes, the underlying problem (the key still shifts when the array shifts) does not.
Verify the fix#
Linting with eslint-plugin-react 7.37.3 on ESLint 9.17.0 (flat config, react/no-array-index-key: 'error'), 16 October 2026:
--- LINT bad-index-key.js ---
/app/bad-index-key.js
1:54 error Do not use Array index in keys react/no-array-index-key
✖ 1 problem (1 error, 0 warnings)Linting the same list keyed by child.id instead (good-id-key.js) with the identical config produces no output and exits 0 — the negative control confirming the rule only fires on a key derived from array position, not on every .map(). To check the runtime behaviour rather than the lint rule: type into an input in one row, delete a row above it, and confirm the text stays attached to its own row instead of jumping to the next one. That is exactly what the keyMode=id run showed earlier — the remaining input stayed empty after the removal, instead of inheriting the deleted row's text.
One email a month — the latest explained errors
What the site published that month — mostly Next.js, Supabase and Node errors, explained. You confirm by email; unsubscribe any time in one click.
When index as a key is still acceptable#
The rule has one documented exception, and it is narrow: a list that is never reordered, never filtered, and never has items inserted or removed — React's docs use a poem's lines as the example, since a poem's line order is fixed for the life of the component. If that is genuinely true for your list, disable the rule for that one line rather than reach for index keys everywhere:
// eslint-disable-next-line react/no-array-index-key -- static list, never reordered
const lines = poem.lines.map((line, index) => <p key={index}>{line}</p>);Two rationales are worth keeping distinct: the lint rule's own doc cites "unnecessary renders" (ESLint's performance angle); the react.dev Pitfall calls out "subtle and confusing bugs" (the correctness angle); and the accepted answer on the Stack Overflow thread narrows it further — the correctness bug only shows up when a list item owns uncontrolled state, which is exactly what the reproduction above demonstrates.
The next error: duplicate keys once every item has an id#
Replacing index buys a new failure mode if the replacement key is not actually unique. Rendering two siblings with the same key (reproduced with key="dup" on both, React 18.3.1, legacy ReactDOM.render, 16 October 2026) prints:
Warning: Encountered two children with the same key, `dup`. Keys should be unique so that components maintain their identity across updates. Non-unique keys may cause children to be duplicated and/or omitted — the behavior is unsupported and could change in a future version.That is a collision, not a position problem: two rows share a name field used as the sole key, say. The fix is to widen the key (add a second field, or fall back to a generated id), not to loosen it back toward an index.
Two other warnings get blamed on this one without being caused by it. A setState call that resolves after its component unmounted — unrelated to which key strategy a list uses — is its own warning, covered in the unmounted-component state update fix; rule it out separately rather than assuming a key change caused it. A list item's useEffect firing twice in development is Strict Mode's intentional double-invoke, independent of any key choice, covered in the Strict Mode double-invoke fix.
A different class of "React got confused" is a hydration mismatch, not a list key: the server-rendered text differs from what the client renders first, covered in the minified React error #425 breakdown. For broader rendering-cost work once the list itself is correct — Core Web Vitals, bundle size, image loading — see the Next.js performance optimisation guide.
A key is a promise to React about identity, not about order. Once the key tracks the same promise the data already makes — this row is "the apple", not "whatever is currently first" — the warning, and the bug it was warning about, both go away.
Further reading: Fix "only exports components": Fast Refresh ESLint error. Also see Fix: $VARIABLE is not listed as a dependency in turbo.json.
Frequently Asked Questions
Still stuck? Paste your error.
Paste the error message or stack trace — we check it against every fix and tool on the site.
Continue Reading
Fix "only exports components": Fast Refresh ESLint error
The exact ESLint rule that throws this, the six code paths that produce six different wordings, and the one move that actually silences it.
Fix: $VARIABLE is not listed as a dependency in turbo.json
Declare the variable once, in env or globalEnv, and both the lint error and the silent runtime drop under Strict Mode disappear.
Fix Cannot resolve symbol 'Routes': stale JetBrains typings
Why a JetBrains IDE flags Routes from react-router-dom 6 as unknown while the app builds, how to prove which typings file it reads, and the order of steps that stops the old copy coming back.


