Use Stable Keys Instead of Array Index in React Lists

Flags array index, random, and non-unique keys on React lists that reorder or change, which attach state, input values, and focus to the wrong row.

What it flags

Flaggedcomponents/TaskList.tsx
import { useState } from 'react';
import { NewTaskForm } from './NewTaskForm';
import type { Task } from './types';

export function TaskList({ initialTasks }: { initialTasks: Task[] }) {
  const [tasks, setTasks] = useState(initialTasks);

  const addTask = (title: string) =>
    setTasks((current) => [{ id: crypto.randomUUID(), title }, ...current]);

  return (
    <>
      <NewTaskForm onAdd={addTask} />
      <ul>
        {tasks.map((task, index) => (
          // After an insert, a note typed in row 1 stays in row 1
          <li key={index}>
            {task.title}
            <input aria-label={`Note for ${task.title}`} />
          </li>
        ))}
      </ul>
    </>
  );
}
Passescomponents/TaskList.tsx
import { useState } from 'react';
import { NewTaskForm } from './NewTaskForm';
import type { Task } from './types';

export function TaskList({ initialTasks }: { initialTasks: Task[] }) {
  const [tasks, setTasks] = useState(initialTasks);

  const addTask = (title: string) =>
    setTasks((current) => [{ id: crypto.randomUUID(), title }, ...current]);

  return (
    <>
      <NewTaskForm onAdd={addTask} />
      <ul>
        {tasks.map((task) => (
          <li key={task.id}>
            {task.title}
            <input aria-label={`Note for ${task.title}`} />
          </li>
        ))}
      </ul>
    </>
  );
}

Why it matters

React matches list items between renders by key, so an index key ties component state, uncontrolled input values, and focus to a position instead of an item. When a row is inserted at the top, removed, or sorted, text a user typed or an expanded panel shows up on a different row. Keys generated during render change every time, which remounts every row and discards its state and DOM. The bug appears only when the list changes, so a static demo in review looks correct.

Agent instructions

cubic checks every pull request against these instructions. Paste them into AGENTS.md or Cursor rules and your AI coding tool follows them while it writes code.

AGENTS.md

Add this section to AGENTS.md at the repository root. Codex, Cursor, and other AI coding tools read it.

## Use Stable Keys Instead of Array Index in React Lists

Applies to `**/*.tsx`, `**/*.jsx`.

Review React list rendering for keys that do not identify the same item across renders.

Flag changes that:
- Use the map index as the key (key={index}, key={i}) on lists that can be sorted, filtered, or have items inserted or removed before the end, especially when rows hold local state, uncontrolled inputs, or focus.
- Generate keys during render with Math.random(), Date.now(), crypto.randomUUID(), or a counter, which remounts every row on each render.
- Build keys from fields that are not unique, such as a name, title, or date, so two items with the same value collide.
- Include editable fields in the key (key={item.name}, key={`${item.id}-${item.updatedAt}`}), so editing a row remounts it and drops its state and focus.
- Combine the index with other data (key={`${item.type}-${index}`}), which still changes when the order changes.

Do not flag:
- Index keys on lists that never reorder, filter, or change length and hold no state, such as a fixed set of labels or skeleton placeholders.
- IDs generated once when an item is created, such as crypto.randomUUID() called in the handler that adds an unsaved row.
- Composite keys built from stable fields, such as `${orderId}-${lineNumber}`, when no single ID exists.

Checks files matching **/*.tsx, **/*.jsx.

More React agents

See all React code review standards

Run this agent on every pull request

cubic reviews every pull request against the agents you turn on and comments on the lines that break them.

Catch this in your code