DEV Community

Anas Sheikh
Anas Sheikh

Posted on

Stop Using Array Index as a React Key. The Bug You Create Might Not Be Where You Think.

Here's a React bug that's incredibly easy to write because the code looks completely reasonable.

You have a list:

{users.map((user, index) => (
  <UserCard
    key={index}
    user={user}
  />
))}
Enter fullscreen mode Exit fullscreen mode

ESLint is happy.

React stops complaining about missing keys.

Your UI renders perfectly.

You ship it.

Then three weeks later someone adds sorting, filtering, deleting, drag-and-drop, or live updates.

Suddenly a checkbox belongs to the wrong user.

An input keeps someone else's value.

An animation happens on the wrong row.

A component's local state appears to randomly move around.

And you start debugging the database.

The database is fine.

React is doing exactly what you told it to do.

The problem is the key.

What React Actually Uses a Key For

A lot of developers think this:

key={user.id}
Enter fullscreen mode Exit fullscreen mode

is basically an optimization hint.

It isn't.

Keys help React determine which item is which between renders.

Think about this list:

Alice
Bob
Charlie
Enter fullscreen mode Exit fullscreen mode

with:

key={user.id}
Enter fullscreen mode Exit fullscreen mode

React can identify:

Alice   → 101
Bob     → 102
Charlie → 103
Enter fullscreen mode Exit fullscreen mode

Now imagine Alice is removed:

Bob
Charlie
Enter fullscreen mode Exit fullscreen mode

React can understand:

101 disappeared
102 is still here
103 is still here
Enter fullscreen mode Exit fullscreen mode

That's exactly what you want.

But now look at:

key={index}
Enter fullscreen mode Exit fullscreen mode

Initially:

Alice   → 0
Bob     → 1
Charlie → 2
Enter fullscreen mode Exit fullscreen mode

Remove Alice.

Your new list becomes:

Bob     → 0
Charlie → 1
Enter fullscreen mode Exit fullscreen mode

From React's perspective, something very different happened.

The component that used to represent Alice now represents Bob.

The component that used to represent Bob now represents Charlie.

The identities shifted.

And that's where the weirdness starts.

The Classic Example: A Checkbox

Imagine:

function UserCard({ user }) {
  const [selected, setSelected] = useState(false);

  return (
    <div>
      <input
        type="checkbox"
        checked={selected}
        onChange={() => setSelected(!selected)}
      />

      {user.name}
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

Your list:

{users.map((user, index) => (
  <UserCard
    key={index}
    user={user}
  />
))}
Enter fullscreen mode Exit fullscreen mode

The user checks:

☑ Alice
☐ Bob
☐ Charlie
Enter fullscreen mode Exit fullscreen mode

Then Alice gets removed.

Your data is now:

Bob
Charlie
Enter fullscreen mode Exit fullscreen mode

But React can reuse the existing component instances.

You might end up visually seeing:

☑ Bob
☐ Charlie
Enter fullscreen mode Exit fullscreen mode

Bob never selected himself.

The component state simply followed the position instead of the user.

This is the kind of bug that makes developers say:

"I swear React is broken."

React isn't broken.

Your identity model is.

Why It Doesn't Happen Immediately

This is the important part.

If your list only ever grows:

Alice
Bob
Charlie
David
Enter fullscreen mode Exit fullscreen mode

index keys may appear to work perfectly.

Nothing obviously breaks.

That's why this mistake survives code review.

The problem appears when the list changes in the middle.

For example:

remove
insert
sort
filter
reorder
drag-and-drop
pagination
live updates
Enter fullscreen mode Exit fullscreen mode

The moment item positions change, index-based identity becomes unreliable.

Sorting Makes It Even More Obvious

Imagine:

1. Laptop
2. Keyboard
3. Mouse
Enter fullscreen mode Exit fullscreen mode

Each row has:

const [expanded, setExpanded] = useState(false);
Enter fullscreen mode Exit fullscreen mode

The user expands:

Laptop
Enter fullscreen mode Exit fullscreen mode

Then they sort by price.

The new order becomes:

Mouse
Laptop
Keyboard
Enter fullscreen mode Exit fullscreen mode

If you're using:

key={index}
Enter fullscreen mode Exit fullscreen mode

React sees:

0 → something
1 → something
2 → something
Enter fullscreen mode Exit fullscreen mode

The positions still exist.

But the actual items changed.

Now the expanded state can appear attached to the wrong product.

With:

key={product.id}
Enter fullscreen mode Exit fullscreen mode

React knows:

Laptop is still Laptop.
It just moved.
Enter fullscreen mode Exit fullscreen mode

That's the distinction.

The Fix Is Usually Ridiculously Simple

Instead of:

{products.map((product, index) => (
  <ProductCard
    key={index}
    product={product}
  />
))}
Enter fullscreen mode Exit fullscreen mode

use:

{products.map((product) => (
  <ProductCard
    key={product.id}
    product={product}
  />
))}
Enter fullscreen mode Exit fullscreen mode

Now the identity belongs to the data.

Not the position.

"But My Data Doesn't Have an ID"

This is where developers sometimes reach for:

key={Math.random()}
Enter fullscreen mode Exit fullscreen mode

Please don't.

That's arguably worse.

Every render can generate a completely different key.

React may interpret your components as entirely new components.

That can cause:

  • unnecessary remounts
  • lost component state
  • broken animations
  • unnecessary DOM work
  • confusing lifecycle behavior

You want a key that's:

stable + unique among siblings

Good:

key={user.id}
Enter fullscreen mode Exit fullscreen mode

Potentially good:

key={user.email}
Enter fullscreen mode Exit fullscreen mode

if the email is guaranteed unique and stable.

Bad:

key={index}
Enter fullscreen mode Exit fullscreen mode

for dynamic lists.

Very bad:

key={Math.random()}
Enter fullscreen mode Exit fullscreen mode

What About crypto.randomUUID()?

You might think:

key={crypto.randomUUID()}
Enter fullscreen mode Exit fullscreen mode

solves the problem.

It doesn't if you're generating it during rendering.

For example:

{items.map(item => (
  <Item
    key={crypto.randomUUID()}
    item={item}
  />
))}
Enter fullscreen mode Exit fullscreen mode

You've created a new identity every time React renders.

That's not stable.

If you need IDs for newly created data, generate them when the item is created, store them with the item, and reuse them.

For example:

const newTodo = {
  id: crypto.randomUUID(),
  text: "Learn React",
};
Enter fullscreen mode Exit fullscreen mode

Then:

<Todo
  key={todo.id}
  todo={todo}
/>
Enter fullscreen mode Exit fullscreen mode

Now the identity survives re-renders.

There Is One Situation Where Index Keys Are Fine

This is where I don't want to turn this into:

"Never ever use index as a key."

That's too simplistic.

Index keys can be acceptable when the list is genuinely static.

For example:

const weekdays = [
  "Monday",
  "Tuesday",
  "Wednesday",
  "Thursday",
  "Friday",
];
Enter fullscreen mode Exit fullscreen mode

If the list:

  • never changes
  • never gets reordered
  • never gets filtered
  • never gets inserted into
  • never gets deleted from

then:

key={index}
Enter fullscreen mode Exit fullscreen mode

is unlikely to create an identity bug.

The important question isn't:

"Is index always bad?"

It's:

"Can the position of this item change?"

If yes, use a stable identity.

The Hidden Cost Goes Beyond Bugs

There's another reason I care about this.

Bad keys make React's reconciliation less meaningful.

React needs to answer:

"Is this the same component as before?"

Stable keys give it a reliable answer.

Bad keys force React to infer identity from position.

And position isn't identity.

This matters especially in interfaces with:

forms
animations
drag-and-drop
editable tables
shopping carts
filters
sorting
live data
virtualized lists
Enter fullscreen mode Exit fullscreen mode

The more interactive the list becomes, the more important stable identity becomes.

Next.js Doesn't Change This Rule

You might be building with:

Next.js
App Router
Server Components
TypeScript
Tailwind
Enter fullscreen mode Exit fullscreen mode

It doesn't matter.

Once React is rendering a dynamic list, the key still needs to represent the item's identity.

For example:

export default function Orders({
  orders,
}: {
  orders: Order[];
}) {
  return (
    <div>
      {orders.map((order) => (
        <OrderRow
          key={order.id}
          order={order}
        />
      ))}
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

That's much safer than:

{orders.map((order, index) => (
  <OrderRow
    key={index}
    order={order}
  />
))}
Enter fullscreen mode Exit fullscreen mode

Here's the Mental Model I Wish More Developers Used

Don't think:

key = React requirement
Enter fullscreen mode Exit fullscreen mode

Think:

key = component identity
Enter fullscreen mode Exit fullscreen mode

Then the correct implementation becomes much more obvious.

If you have:

User 42
Enter fullscreen mode Exit fullscreen mode

its identity is:

42
Enter fullscreen mode Exit fullscreen mode

If it moves from position 4 to position 1, it's still User 42.

Therefore:

key={user.id}
Enter fullscreen mode Exit fullscreen mode

makes sense.

But if you use:

key={index}
Enter fullscreen mode Exit fullscreen mode

you're effectively saying:

"This component's identity is whatever happens to be sitting in this position."

That's usually not what you mean.

A Quick Code Review Trick

Whenever I see:

.map((item, index) => ...)
Enter fullscreen mode Exit fullscreen mode

I immediately look at why index exists.

If the only reason is:

key={index}
Enter fullscreen mode Exit fullscreen mode

I ask:

"Does this item have a stable ID?"

Usually the answer is yes.

Then the code becomes:

.map((item) => (
  <Component
    key={item.id}
    item={item}
  />
))
Enter fullscreen mode Exit fullscreen mode

One less variable.

One better identity model.

Potentially one nasty bug avoided.

The Bug Is Often Far Away From the Key

This is why these bugs are so frustrating.

The code that creates the problem might be:

key={index}
Enter fullscreen mode Exit fullscreen mode

But the symptom could appear somewhere completely different:

❌ Wrong checkbox
❌ Wrong input value
❌ Wrong animation
❌ Wrong expanded row
❌ Wrong selected item
❌ Wrong focus
❌ State apparently "moving"
Enter fullscreen mode Exit fullscreen mode

So when you see strange behavior in a dynamic React list, don't only inspect the state logic.

Look at the keys.

Especially if the list can be reordered, filtered, inserted into, or deleted from.


My React Key Rule

I keep it simple:

Static list?
→ Index can be okay.

Dynamic list?
→ Use a stable ID.

User-generated data?
→ Give each item a stable ID when it's created.

Never generate keys during render.
Enter fullscreen mode Exit fullscreen mode

And most importantly:

A key isn't there to make React stop complaining.

It's there to tell React:

"This thing is still the same thing."

Once you understand that, React keys stop feeling like annoying syntax and start making a lot more sense.


Have you ever had one of those React bugs where the data was completely correct but the UI was showing the wrong state?

There's a good chance the first thing worth checking was the key.

If you've encountered a weird key-related bug in production, share it below. I'd love to hear the strangest one.

Get the templates: https://pixelanas.gumroad.com


Anas, full-stack Next.js developer building SaaS products and premium templates. X: *@ASheikh69751

Top comments (0)