albert.walickihire me
← all solutions
javascript

Why you shouldn't use index as a key in React

Using the array index as a key makes React attach state to the wrong list items after you add, remove or reorder them. Here's why, and what to use instead.

Keys tell React which item is which between renders. With the index as the key, a key belongs to a position in the list, not to an item. When you add an item at the start, remove one or reorder the list, the positions shift, and React gives each item the DOM nodes and state of whatever used to be at its position. Use a stable ID from your data instead:

{todos.map((todo) => (
  <TodoItem key={todo.id} todo={todo} />
))}

What goes wrong with index keys

Here's a list with index keys and a checkbox in each row:

import { useState } from 'react';

const initialTodos = [
  { id: 'a1', text: 'Buy milk' },
  { id: 'b2', text: 'Walk the dog' },
];

export default function TodoList() {
  const [todos, setTodos] = useState(initialTodos);

  const addTodo = (text) => {
    const todo = { id: crypto.randomUUID(), text };
    setTodos((prev) => [todo, ...prev]);
  };

  return (
    <>
      <button onClick={() => addTodo('Call the vet')}>
        Add to top
      </button>
      <ul>
        {todos.map((todo, index) => (
          <li key={index}>
            <label>
              <input type="checkbox" /> {todo.text}
            </label>
          </li>
        ))}
      </ul>
    </>
  );
}

Tick "Buy milk", then click "Add to top". The list now reads "Call the vet", "Buy milk", "Walk the dog", but the tick is next to "Call the vet".

Before the click, key 0 was "Buy milk". After it, key 0 is "Call the vet". React sees that key 0 still exists, so it keeps that <li> and its checkbox, which is still ticked because React doesn't control it, and only updates the text. Key 2 is new, so the last row gets a fresh checkbox.

The same happens to typed text in inputs, to focus, and to useState inside child components. Nothing throws an error, which makes it easy to ship: the data is right, but the UI shows it next to the wrong item.

The fix is one line:

<li key={todo.id}>

Now the ticked checkbox moves with "Buy milk".

Where the ID should come from

  • Data from an API or a database usually has an id already. Use it, or another unique field such as a slug.
  • For items created in the browser, generate the ID once, when the item is created, like addTodo does above. crypto.randomUUID() works in modern browsers on HTTPS and localhost (more on generating UUIDs).
  • If data arrives without IDs, add them right after you load it, not while rendering.

Never create the key during render:

// Don't: every render creates new keys
{todos.map((todo) => (
  <li key={Math.random()}>{todo.text}</li>
))}

A key that changes on every render tells React that every item is new every time. React throws away the old DOM nodes and state and mounts new ones, so inputs lose what was typed, focus jumps, and the list renders slower. key={crypto.randomUUID()} in the JSX has exactly the same problem.

When the index is fine

The index is safe when all of these are true:

  • the list is static, or only ever grows at the end,
  • it's never sorted, filtered or reordered,
  • the items have no state: no inputs and no stateful child components.

A breadcrumb or a few paragraphs from a CMS are good examples. If you're not sure, use an ID. React also uses the index when you leave key out, which is why it warns you about it, and eslint-plugin-react has a react/no-array-index-key rule if you want your linter to catch it.

Keys only need to be unique among siblings

Two different lists on the same page can both use 1 as a key. The key goes on the outermost element you return from map, not inside the child component, and the component doesn't receive key as a prop.

When you map to several elements without a wrapper, use Fragment with a key, because the short <> syntax can't take one:

import { Fragment } from 'react';

function Glossary({ terms }) {
  return (
    <dl>
      {terms.map((term) => (
        <Fragment key={term.id}>
          <dt>{term.name}</dt>
          <dd>{term.definition}</dd>
        </Fragment>
      ))}
    </dl>
  );
}

The same mechanism is useful on purpose: changing a component's key resets it. <ProfileForm key={userId} /> gives you a fresh form, with empty state, for every user.

more solutions
work with me

Got something that needs building?

Frontend builds, full-stack features in Django, design-system work. Available for work.

See my workGet in touch