Next.js

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.

Mahdi Ben RhoumaMahdi Ben Rhouma
9 min read

TL;DR#

text
/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#

jsx
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:

text
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): true

With 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.

  1. Use an id the data already has. Most API responses and database rows already carry one.

    jsx
    const list = children.map((child) => <li key={child.id}>{child.label}</li>);
  2. 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 like uuid), and keep that id attached to the object from then on.

  3. 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 name concatenated with a recipeId only 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:

text
/app/bad-index-key-prefixed.js
  1:54  error  Do not use Array index in keys  react/no-array-index-key

key={`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:

text
--- 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:

jsx
// 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:

text
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

|

Have more questions? Contact us

Still stuck? Paste your error.

Paste the error message or stack trace — we check it against every fix and tool on the site.

Matching happens in your browser. When you search, a cleaned error signature is sent so we can spot errors we do not cover yet — never the raw text, your IP address or your e-mail. Do not paste passwords or keys. Details

Written by

Mahdi Ben Rhouma
Mahdi Ben Rhouma

Full-Stack Dev — Next.js & Supabase

Solo developer building SaaS products with Next.js and Supabase. Writing about production patterns the official docs skip.

Remote