Списки и ключи (keys)

При рендеринге массива компонентов React требует уникальный атрибут key для каждого элемента — он помогает алгоритму reconciliation эффективно определять какие элементы изменились, добавились или удалились.

Зачем нужно

Без key React не может различить элементы списка: при добавлении нового элемента в начало он перерисовывает весь список заново. С key React находит изменения точечно: перемещает, обновляет или удаляет только нужные элементы. Неправильный key (индекс массива) при сортировке или фильтрации вызывает баги: state компонентов «перескакивает» между элементами.

Где используется

  • Рендеринг любых списков через .map: товары, пользователи, комментарии
  • Динамические списки с добавлением/удалением/сортировкой
  • Динамические табы, секции, компоненты

Правила для key

// ✓ Хорошо — стабильный уникальный идентификатор из данных
{users.map(user => (
  <UserCard key={user.id} user={user} />
))}

// ✓ Хорошо — уникальная строка (slug, uuid)
{posts.map(post => (
  <PostCard key={post.slug} post={post} />
))}

// ✗ Плохо — индекс массива при изменяемом списке
{items.map((item, index) => (
  <Item key={index} item={item} />  // баги при сортировке/добавлении
))}

// △ Допустимо — индекс для статических неизменяемых списков
{['Январь', 'Февраль', 'Март'].map((month, i) => (
  <li key={i}>{month}</li>  // список не изменится — индекс OK
))}

Почему индекс опасен

// Список с инпутами
function TodoList() {
  const [todos, setTodos] = useState([
    { id: 1, text: 'Купить молоко' },
    { id: 2, text: 'Позвонить другу' },
  ]);

  const addAtBeginning = () => {
    setTodos(prev => [{ id: Date.now(), text: 'Новая задача' }, ...prev]);
  };

  return (
    <>
      <button onClick={addAtBeginning}>Добавить в начало</button>
      <ul>
        {todos.map((todo, index) => (
          // key={index}: при добавлении в начало
          // React думает что первый элемент НЕ изменился (key=0 тот же)
          // Но теперь key=0 → 'Новая задача', а input помнит 'Купить молоко'
          // → state инпута «перескакивает» на новый элемент (БАГ!)
          <li key={index}>
            <span>{todo.text()}</span>
            <input placeholder="Заметка к задаче" />
          </li>
        ))}
      </ul>
    </>
  );
}

// Правильно: key={todo.id} — React отслеживает по id, не по позиции

key как механизм сброса state

// Хак: изменение key полностью пересоздаёт компонент
// Полезно для сброса state при смене данных

function UserEditor({ userId }) {
  return (
    // При смене userId компонент пересоздаётся с нуля
    // Не нужно вручную сбрасывать state формы
    <EditForm key={userId} userId={userId} />
  );
}

Компонент-обёртка при рендеринге массива

// Если не нужен реальный DOM-элемент для группировки
import { Fragment } from 'react';

function DefinitionList({ terms }) {
  return (
    <dl>
      {terms.map(term => (
        // Fragment с key — не добавляет лишний DOM-элемент
        <Fragment key={term.id}>
          <dt>{term.word}</dt>
          <dd>{term.definition}</dd>
        </Fragment>
      ))}
    </dl>
  );
}

Частые ошибки

  • key={index} при сортируемых/фильтруемых списках — state компонентов привязывается к позиции, а не к данным; используйте id из данных.
  • Генерируют key динамическиkey={Math.random} или key={Date.now()} в рендере создают новый ключ при каждом рендере → все элементы пересоздаются.
  • key на неверном уровнеkey должен быть на корневом элементе внутри .map, а не на дочернем.
  • key не уникален в рамках списка — дублирующиеся key → непредсказуемое поведение.

Связанные темы

Ресурсы