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 и ре-рендеров
Рендеринг списков и keys

0

1

Войти