DEV Community

Devanshu Biswas
Devanshu Biswas

Posted on • Originally published at github.com

I built a tool that shows why array-index keys break React state (reorder a list and watch it happen)

"Don't use the array index as your key" is the most-repeated rule in React and the least-shown. Everyone quotes it; almost no demo lets you watch the bug happen. So I built one where you type notes into a list, reorder it, and watch your notes get stranded on the wrong items.

▶ Live demo: https://list-key-reconciliation.vercel.app/
Source (React 19 + TS): https://github.com/dev48v/list-key-reconciliation

What key actually does

When a list re-renders, React has to match the previous children to the next ones to decide what to keep, move, or throw away. key is the identity it matches on. Give it the array index and you're telling React "the thing at position 0 is the same thing as before" — even after you've reordered the array and position 0 holds a completely different item.

items.map((item, i) => <Row key={i} item={item} />)        // 🐞 identity = position
items.map((item)    => <Row key={item.id} item={item} />)  // ✅ identity = the item
Enter fullscreen mode Exit fullscreen mode

The proof

Each row in the demo is a component with its own local state — a note you type — plus a stable "instance #" it grabs when it mounts. Type milk next to Groceries, rent next to Pay rent, and so on. Now hit Reverse.

With key={index}:

  • The instance # column doesn't change order — 1, 2, 3, 4 top to bottom, before and after. That's the tell: React reused each instance by position.
  • Your notes don't move either. So milk is now sitting next to Book flights. The data reversed; the instance state didn't.

Switch to key={item.id}, reset, re-type, reverse again:

  • The instance # now travels with its label, and milk stays glued to Groceries. React matched by id, saw the item moved, and moved the instance — DOM node, local state, and all.

A mount counter confirms the subtle part: reordering with index keys doesn't cause extra mounts. It's not remounting — it's the same instance reused for a different item. That's exactly why it's so sneaky; nothing errors, nothing flashes, the state just quietly belongs to the wrong row.

The note is a stand-in for real damage

A typed note is easy to see, but it represents everything that lives in the instance and not in your data:

  • uncontrolled <input> / <textarea> values
  • which element has focus (keyboard users land on the wrong field)
  • scroll position within a row
  • in-flight CSS transitions and animations
  • any useState / useRef inside the row component
  • a non-reset <video>/<canvas>, an open dropdown, a half-filled form

Reorder a list keyed by index and all of that stays pinned to the slot while the data slides past it.

So is index ever okay?

Yes — if the list is static: never reordered, never filtered, items only ever appended/removed at the end. Then an item's index never changes while it's alive, and index-as-key is fine (and saves you inventing ids). The moment an item's index can change mid-life, use a stable id.

Open the demo, type into the rows, and flip between the two key modes while reordering. Once you've watched milk jump to the wrong task, you'll never index-key a dynamic list again.

If it made keys click, a star helps others find it: https://github.com/dev48v/list-key-reconciliation

Top comments (0)