React
Рендеринг списков и keys
Лента Twitter, список писем в Gmail, корзина в интернет-магазине - всё это списки, которые постоянно меняются: приходят новые элементы, удаляются старые, меняется порядок. Без правильных ключей React при таком обновлении путается: оставляет введённый текст не в том поле, проигрывает анимацию не у того элемента, теряет позицию скролла. Один маленький атрибут key решает, увидит ли пользователь плавное обновление списка или хаос из перепутанных строк. Понимание key отделяет работающий список от загадочно глючащего.
- Бесконечная лента в Instagram и Twitter дорисовывает элементы при скролле - стабильные ключи держат уже отрендеренные строки на месте
- Список задач или чат: при удалении одного элемента key позволяет React убрать именно его, а не пересобрать весь список
- Таблицы с сортировкой и фильтрами перетасовывают строки - без key состояние ячеек уезжает не туда
- React в режиме разработки специально предупреждает в консоли о пропущенном key - это одна из самых частых ошибок новичков
Предварительные знания
- Компоненты и props: один компонент рендерится с разными данными
- Метод массива map и стрелочные функции в JavaScript
- Идея reconciliation из вводного урока: React сравнивает старое и новое описание UI
Массив данных в массив элементов
В JSX нет цикла for, потому что в фигурных скобках живут только выражения. Зато массивы умеют возвращать новый массив через map, а это уже выражение. Поэтому стандартный способ отрисовать список - вызвать map на массиве данных и вернуть из колбэка JSX-элемент для каждого. React умеет рендерить массив элементов как набор соседних узлов.
Колбэк map возвращает не строку, а полноценный JSX-элемент. Часто это вызов отдельного компонента: products.map(p => <ProductCard key={p.id} product={p} />). Так список из примитивных тегов превращается в список карточек, и каждая карточка получает свои данные через props.
Если массив пустой, map вернёт пустой массив, и React просто ничего не отрисует. Отдельную проверку на пустоту делают только когда нужно показать заглушку вроде 'Список пуст'.
Почему для рендера списка используют map, а не цикл for внутри JSX?
Зачем нужен key
При ре-рендере React сравнивает старый список элементов с новым, чтобы внести в DOM минимум изменений. Но как понять, что элемент 'тот же самый', а не новый? По позиции это ненадёжно: если в начало списка добавить элемент, все остальные сдвинутся. Атрибут key даёт каждому элементу устойчивую личность. По нему React сопоставляет элементы между рендерами и точно знает, что добавилось, удалилось или просто переместилось.
key - это служебная подсказка для React, а не обычный prop. Внутри компонента прочитать props.key нельзя: React забирает key себе для сопоставления и не передаёт его дальше. Если значение нужно и внутри, его передают вторым атрибутом под другим именем.
Хороший key стабилен и уникален в пределах списка. Идеальный источник - идентификатор из данных: id записи в базе, уникальный slug, email. Такой ключ не меняется при пересортировке и не повторяется. Если стабильного id в данных нет, его стоит сгенерировать один раз при создании элемента и хранить вместе с данными.
| Источник key | Стабильный | Подходит |
|---|---|---|
| id из базы данных | Да | Да, лучший вариант |
| Уникальный slug или email | Да | Да, если гарантированно уникален |
| Индекс массива | Нет | Только для статичных, неизменяемых списков |
| Math.random() | Нет | Нет, ломает сопоставление на каждом рендере |
Math.random() или Date.now() в роли key - распространённая ошибка. Такой ключ меняется на каждом рендере, поэтому React считает все элементы новыми, выбрасывает старые узлы DOM и создаёт заново. Это убивает производительность и сбрасывает состояние элементов.
Зачем React нужен атрибут key у элементов списка?
Баг с индексом в роли key
Соблазнительно взять второй аргумент map - индекс - и поставить его как key. Для неизменного списка это безвредно. Но как только список меняет порядок, в начало добавляется или из середины удаляется элемент, индексы перестают соответствовать данным. Элемент с key равным 0 - это уже другая запись, чем была. React решает, что данные у элемента просто изменились, и оставляет на месте старый узел DOM вместе с его внутренним состоянием.
Конкретный сценарий бага: пользователь ввёл текст в поле третьей строки, затем удалил первую строку. Все элементы сдвинулись вверх на одну позицию, но индексы остались 0, 1, 2. React по индексу решает, что третья строка - это та же, что и раньше, и не трогает её DOM-узел. В результате введённый текст остаётся в поле, которое теперь относится к совсем другой записи данных.
Индекс в роли key допустим в одном случае: список статичен, никогда не меняет порядок, в него не добавляют и из него не удаляют элементы. Если хоть одно из условий нарушается, нужен стабильный id из данных.
Почему индекс массива как key ломает список, в котором удаляют элементы?
Связь с другими темами
Списки - первое место, где модель reconciliation становится заметной на практике:
- Компоненты и props — Каждый элемент списка - это вызов одного компонента с уникальными props
- Что такое render — Сопоставление элементов по key - часть фазы reconciliation при ре-рендере
- useState — Списки чаще всего хранятся в состоянии и меняются иммутабельно при добавлении и удалении
Итог
- Список рендерится через map: массив данных превращается в массив JSX-элементов прямо внутри фигурных скобок
- Каждому элементу списка нужен prop key - стабильный уникальный идентификатор
- key нужен для reconciliation: по нему React сопоставляет элементы между перерисовками и понимает, что добавилось, удалилось или переместилось
- Хороший key - это id из данных. Плохой - индекс массива, если список может меняться порядком, добавлением или удалением
- Индекс как key порождает баги: введённый текст и состояние компонентов уезжают не к тем строкам при изменении списка
Связанные уроки
- rc-03-components-props — Список - это один компонент, отрисованный много раз с разными props, поэтому компоненты нужны первыми
- rc-02-jsx — map внутри JSX - это выражение в фигурных скобках, разобранное в уроке про JSX
- rc-10-render-mental-model — Понимание того, как React сопоставляет элементы по key, прямо ведёт к модели reconciliation и ре-рендеров