React
Keys вглубь: identity и баги состояния
Список задач с чекбоксами и полями ввода. Разработчик ставит индекс массива как key, всё работает - до первого удаления элемента из середины. Тогда галочки и введённый текст внезапно перепрыгивают на чужие строки: отметили одну задачу, отметилась другая. Поведение выглядит как мистика, но причина строгая - key как индекс привязал идентичность к позиции, а не к данным. Тот же механизм, понятый правильно, можно использовать наоборот: одним key намеренно сбросить состояние компонента.
- Формы в списках: текст в input или состояние чекбокса перепрыгивает на соседние строки после удаления элемента с index-key
- Профили и табы: при переключении пользователя форму редактирования сбрасывают через key={userId}, чтобы поля не показывали чужие данные
- Анимации списков: framer-motion и react-spring опираются на стабильные keys, чтобы анимировать вход и выход элементов
- Виртуализированные списки: TanStack Virtual и подобные требуют корректных keys, иначе переиспользование строк ломает состояние
- Сброс плеера или редактора: смена key на медиакомпоненте полностью перемонтирует его при выборе нового трека или документа
Предварительные знания
- Reconciliation: сопоставление элементов по типу и позиции, потеря состояния при смене типа
- Рендеринг списков через map и базовое назначение пропа key
- Понимание того, что состояние компонента живёт между рендерами, пока компонент не размонтирован
Key - это идентичность, а не порядковый номер
Key отвечает на единственный вопрос: какой элемент нового списка соответствует какому элементу старого. Это идентичность, а не порядок и не индекс. React хранит состояние и DOM каждого элемента, привязывая их к его key. На следующем рендере он ищет элемент с тем же key и переиспользует для него существующее состояние, даже если элемент сменил позицию. Если же key исчез, React считает элемент удалённым, а новый key - новым элементом.
Поэтому хороший key обязан быть стабильным и уникальным среди соседей. Стабильным - значит один и тот же для одной сущности на всех рендерах: id записи из базы, id сообщения, уникальный slug. Уникальным среди соседей - значит не повторяющимся внутри одного списка. Важно, что key уникален локально, в пределах своего массива, а не глобально по всему приложению.
| Кандидат в key | Годится | Почему |
|---|---|---|
| item.id из базы данных | Да | Стабилен и уникален, привязан к самой сущности |
| Индекс массива | Нет (кроме статичных списков) | Привязан к позиции, ломается при перестановке и вставке |
| Math.random() на рендере | Нет | Меняется каждый рендер, заставляет пересоздавать все элементы |
| Составной ключ из полей данных | Да, если уникален | Подходит, когда отдельного id нет, но комбинация полей однозначна |
Math.random() или Date.now() в качестве key - тихая катастрофа для производительности. Новый key на каждом рендере означает, что React считает все элементы новыми: размонтирует старые, монтирует новые, теряет состояние и пересоздаёт весь DOM списка каждый раз. Key обязан быть детерминированным для одной и той же сущности.
Что на самом деле выражает key элемента списка?
Классический баг с индексом как key
Самая частая ловушка - использовать индекс массива как key в списке, который меняет порядок, или из которого удаляют и в который вставляют элементы. Пока список статичен, индекс совпадает с идентичностью и всё работает. Но как только элемент удаляют из середины, индексы оставшихся сдвигаются: бывший третий становится вторым. Для React, который опознаёт элементы по key, это выглядит так, будто содержимое второй позиции изменилось, а последняя позиция исчезла.
Результат на экране выглядит как мистика: удалили первую задачу, а текст, который был введён во второй строке, теперь показывается в первой. Причина в том, что React сопоставил состояние по key=index: состояние, привязанное к key 0, осталось на позиции 0, но данные на этой позиции теперь другие. Состояние и данные разъехались, потому что индекс - это про позицию, а не про сущность.
Индекс как key допустим только при трёх условиях одновременно: список статичен (не меняет порядок), элементы не добавляются и не удаляются из середины, и у элементов нет собственного состояния или неуправляемых полей ввода. Чисто отображаемый неизменный список - единственный безопасный случай. В любом динамическом списке нужен стабильный id.
Почему индекс массива как key вызывает баги при удалении элемента из середины списка?
Key как инструмент сброса состояния
Та же механика, что вызывает баги при неаккуратном key, превращается в полезный приём, если применить её намеренно. Раз React считает элемент с новым key другим элементом и пересоздаёт его с нуля, сменой key можно умышленно сбросить всё внутреннее состояние компонента. Это чище, чем вручную обнулять каждое поле через эффект, и работает на любом состоянии разом.
Без этого приёма при переключении userId форма показывала бы данные предыдущего пользователя, пока их не затрут вручную. Соблазн сделать сброс через useEffect, который следит за userId и обнуляет все поля, ведёт к хрупкому коду: легко забыть поле, и появляется лишний рендер. Смена key решает задачу одним движением: React сам выбрасывает старый экземпляр со всем его состоянием.
- Сброс через useEffect — Эффект следит за userId и руками вызывает setName('') и setBio(''). Нужно перечислить каждое поле, легко забыть новое. Происходит лишний рендер: сначала со старыми значениями, затем со сброшенными
- Сброс через key — key={userId} заставляет React пересоздать компонент. Всё состояние сбрасывается разом, новых полей помнить не нужно. Декларативно и без лишнего промежуточного рендера
Приём со сменой key уместен, когда логически это действительно другая сущность: другой пользователь, другой документ, другой разговор. Не стоит менять key ради мелкого обновления данных той же сущности - это перемонтирует поддерево и без нужды сбросит состояние и DOM. Сброс через key - это про смену идентичности, а не про обновление.
Почему смена key на компоненте сбрасывает всё его внутреннее состояние?
Связь с другими темами
Урок углубляет тему идентичности элементов. Опирается на:
- Reconciliation и Fiber — Keys работают поверх алгоритма сопоставления: они подменяют позиционную идентичность явной
- Рендеринг списков и keys — Базовое знакомство с keys, которое здесь расширяется до тонких багов и приёмов
- Модель рендера — Объясняет, где живёт состояние и почему смена key его сбрасывает
Итог
- Key - это идентичность элемента между рендерами: он говорит React, что элемент с этим key - тот же самый, где бы он ни оказался в списке
- Индекс массива как key привязывает идентичность к позиции, а не к данным, и при перестановке или вставке состояние перепрыгивает на чужие элементы
- Стабильный уникальный key из данных (id) сопоставляет элементы по содержимому и сохраняет их состояние и DOM при изменениях порядка
- Смена key на элементе - это сигнал React 'это другой элемент': React размонтирует старый и смонтирует новый, полностью сбросив состояние
- Намеренная смена key (например key={userId}) - идиоматичный способ сбросить состояние компонента при смене сущности, без ручной очистки полей
Связанные уроки
- rc-17-reconciliation — Keys имеют смысл только поверх понимания того, как reconciliation сопоставляет элементы по типу и позиции
- rc-04-rendering-lists-keys — Базовое знакомство с keys в списках расширяется здесь до тонких багов идентичности и намеренного сброса состояния
- rc-10-render-mental-model — Сброс состояния через смену key понятен только с моделью рендера: где живёт состояние и когда оно теряется