Ключи в React списках: почему index — плохая идея и что делать вместо этого

Разработчики React постоянно работают с динамическими списками. Рендер массива элементов — рутина, но именно здесь кроется коварная ошибка, которая может привести к катастрофическим последствиям: багам с состоянием, неожиданному поведению компонентов и падению производительности. Всё начинается с безобидной строки: key={index}.

Механика ключей: что React делает под капотом

При ререндере списка React использует ключи для сопоставления элементов до и после обновления. Без ключа React сравнивает деревья путём рекурсивного обхода — дорогостоящей операции. Ключи позволяют React:

  1. Определять идентичность элемента даже при изменениях порядка
  2. Избегать ненужного ререндера стабильных элементов
  3. Сохранять DOM-состояние полей ввода, фокуса и внутреннего состояния компонента

Когда вы используете index, возникает фатальный недостаток: ключ перестаёт соответствовать уникальности и неизменности данных. Рассмотрим классический антипаттерн:

jsx
// Данные с уникальными ID во внутренней структуре
const users = [
  { id: 'u1', name: 'Alice' },
  { id: 'u2', name: 'Bob' },
];

const UserList = () => {
  return (
    <ul>
      {users.map((user, index) => (
        <UserItem 
          key={index}  // ⚠️ Дьявол кроется здесь 
          name={user.name} 
        />
      ))}
    </ul>
  );
};

Взрывной сценарий: что пойдёт не так

Сценарий 1: Удаление элемента посередине списка
Удалим «Alice» из массива. React увидит:

text
До: 
  0: Alice (key=0)
  1: Bob (key=1)

После:
  0: Bob (key=0)

React сопоставит ключи и подумает: «Элемент 0 (Alice) изменился на Bob, а элемент 1 (Bob) пропал». Вместо аккуратного удаления Alice компонент Bob получит неожиданные пропсы, обновит состояние ниже по дереву, а React может назначить неправильные DOM-атрибуты, включая фокус на инпутах.

Сценарий 2: Сортировка
Добавляем фильтр сортировки по имени:

text
До сортировки:
  0: Alice (key=0)
  1: Bob (key=1)

После сортировки (Z-A):
  0: Bob (key=0)    // Реакт "думает", что элемент key=0 изменился с Alice на Bob
  1: Alice (key=1)  // ...а key=1 изменился с Bob на Alice

Компоненты будут ререндериться полностью вместо простой перестановки в DOM. Портится производительность и ломается внутреннее состояние: представьте чекбокс, внезапно перескакивающий на другую строку.

Правильные стратегии генерации ключей

Используйте уникальный бизнес-идентификатор

Если данные приходят с бэкенда, используйте те ID, которые заложены в структуре данных:

jsx
{products.map(product => (
  <ProductCard key={product.id} {...product} />
))}

Генерация клиентских ключей (если своих ID нет)

Когда данные временные или локальные, создайте хэш на основе уникальных свойств:

jsx
const TodoList = ({ todos }) => {
  const generateKey = (todo) => `${todo.timestamp}-${todo.text.substring(0, 5)}`;
  
  return (
    <>
      {todos.map(todo => (
        <TodoItem key={generateKey(todo)} todo={todo} />
      ))}
    </>
  );
};

Криптографически устойчивые ключи

Для чувствительных данных (хотя это редко в UI) применяйте crypto.randomUUID:

jsx
const SessionItems = () => {
  const sessions = useSessions(); // Массив без ID
  return (
    <>
      {sessions.map(session => (
        <div key={crypto.randomUUID()}>{session.data}</div>
      ))}
    </>
  );
};

Когда индекс можно использовать?

Исключения подтверждают правило: – Статические списки, где порядок/количество гарантированно не меняются (например, список языков в настройках) – Данные "только для чтения" без компонентов с внутренним состоянием – Прототипирование, если вы контролируете весь поток данных

Но помните: такую компоненту быстро можно регистрировать больно: добавление состояния или фильтрации запустит баги.

Вы ошиблись? Как отловить проблему

React 18+ выводит строгие предупреждения в консоль:

text
Warning: Each child in a list should have a unique "key" prop.

Но он не различает «уникальный» и «уникальный и стабильный». Добавьте линтер-правило:

.eslintrc.js:

js
rules: {
  'react/no-array-index-key': 'error',
}

Альтернатива для advanced: library-key

В библиотеках с виртуализацией (т.е. react-virtualized) используют ключи, привязанные к позиции данных (например, key={startIndex}). Это искусственный сценарий, где индекс работает, потому что видимый диапазон данных стабилен ниже конкретной границы прокрутки.

Заключение: педантичность окупается

Фраза "используйте уникальные стабильные ключи" звучит как риторика. Но за ней стоит инженерная реальность: неправильные ключи порождают непредсказуемые баги, которые на отладку требуют часов. Ирония в том, что решение простое: либо 10 секунд найти ID, либо добавить хэш. В критичных для производительности списках разница заметна и в профилировщике.

Старожилы React помнят эпоху до введения key, когда ререндеры списков из 100+ элементов блокировали UI на секунды. Современная рекомендация — не регрессия к тем временам, а цена ошибки сейчас дороже из-за сложности SPA.

Проверьте прямо сейчас: в любом списке вашего проекта везде, где есть key={index}, замените его на реальный идентификатор. Если проект вдруг стал стабильнее — вы не один.