Чтобы отрендерить список в React, вызовите map() на массиве и верните кусок JSX для каждого элемента. Дайте внешнему элементу каждого пункта проп key со значением, которое идентифицирует этот пункт, обычно его ID, чтобы React мог различать пункты при изменении списка.
Добавьте { id: 'dat', name: 'Date', price: 4 } в массив, и появится четвёртая строка. Собственного синтаксиса циклов в JSX нет: map возвращает массив элементов, а React рендерит массив, рендеря каждый элемент по порядку.
Рендер массивов через map
map вызывает вашу функцию один раз на каждый элемент и собирает результаты в новый массив. Внутри фигурных скобок JSX этот массив просто значение, поэтому можно вызывать map прямо в разметке, как выше, или сначала собрать массив:
const items = fruits.map((fruit) => <li key={fruit.id}>{fruit.name}</li>);
return <ul>{items}</ul>;
Две детали часто сбивают с толку. Если в стрелочной функции фигурные скобки, нужен return: fruits.map((f) => { return <li key={f.id}>{f.name}</li>; }). Без него функция возвращает undefined, и список пуст. А key ставится на элемент, возвращаемый из map, а не на элемент внутри него. Если пункт это компонент, ставьте ключ на компонент: <FruitRow key={fruit.id} fruit={fruit} />.
Сначала filter, потом map
Чтобы показать часть элементов, сначала отфильтруйте массив, а затем вызовите map для результата. Каждый метод делает одну работу, и цепочка читается как фраза «подходящие фрукты в виде пунктов списка».
Наберите er в поле поиска, и список сузится до Eraser, Ruler и Stapler; отметьте In stock only, и исчезнет ещё Ruler. Отфильтрованный список вычисляется во время рендера из двух значений состояния, поэтому второго массива в состоянии, который нужно синхронизировать, нет. visible.length === 0 && это безопасное использование &&, потому что сравнение даёт настоящее булево значение (страница об условном рендеринге показывает, что бывает с голым 0).
Что делают ключи
Когда список рендерится заново, React сравнивает новый массив элементов с предыдущим. Ключи это способ их сопоставить: элемент с ключом 3 сейчас это элемент с ключом 3 раньше, даже если он переместился. React сохраняет узел DOM этого пункта и состояние его компонента и только перемещает или обновляет то, что изменилось. Пункты с новыми ключами создаются, а пункты с исчезнувшими ключами удаляются.
Без ключей React может сопоставлять только по позиции. Это нормально, пока ничего не двигается, и неправильно, как только что-то сдвинется.
Почему ключи-индексы ломаются
map передаёт индекс вторым аргументом, поэтому key={index} так и просится. Но индекс описывает позицию, а не элемент. Вставьте элемент в начало, и индекс каждого элемента сдвинется на один, поэтому React считает, что элемент 0 по-прежнему элемент 0, и оставляет там его старое состояние.
Демо ниже рендерит один и тот же список дважды: один раз с ключами по индексу, другой по ID. В каждой строке есть поле ввода, которое хранит свой текст.
Напишите math рядом с Ada в обоих списках, затем нажмите Add to top. В списке по ID заметка остаётся у Ada. В списке по индексу заметка перескакивает к новому человеку, потому что первое поле ввода всё ещё поле на позиции 0, а на позиции 0 теперь кто-то другой. Имена в обоих списках правильные, потому что они берутся из данных; ломается только состояние, которое живёт в DOM и в компонентах. Реальные приложения натыкаются на это с флажками, фокусом, анимациями и любым компонентом, который хранит useState.
Ключи-индексы безопасны, только когда выполняется всё сразу: список никогда не переупорядочивается и не фильтруется, элементы никогда не вставляются и не удаляются, кроме как в конце, и у элементов нет собственного состояния. Статичный список ссылок в подвале подходит. Всё, что пользователь может редактировать, обычно нет.
Откуда брать ключи
Лучший ключ это ID, который уже есть в данных: ID из базы данных, артикул товара, slug, имя пользователя. У данных с сервера он почти всегда есть.
Для элементов, созданных в браузере, дайте каждому элементу ID при создании и храните его вместе с элементом: счётчик, как в демо выше, или crypto.randomUUID(). Никогда не генерируйте ключ во время рендера:
// Wrong: a new key on every render, so React remounts every item every time
{todos.map((todo) => <Todo key={Math.random()} todo={todo} />)}
// Right: the ID is created once, in the event that adds the item
function addTodo(text) {
setTodos([...todos, { id: crypto.randomUUID(), text }]);
}
Ключ, который меняется при каждом рендере, хуже индекса: каждый элемент каждый раз уничтожается и создаётся заново, поля ввода теряют фокус, пока вы печатаете, и ничто не сохраняет состояние. Тот же приём полезен намеренно: смена ключа компонента сбрасывает его, и страница о useState использует это, чтобы очистить форму.
Ключи уникальны только среди соседей
Ключ должен быть уникальным внутри одного списка, а не во всём приложении. Два отдельных списка могут повторно использовать одни и те же ID, потому что React сравнивает ключи только среди детей одного родителя.
В обоих разделах есть блюда с ключами 1 и 2, и это правильно: каждый ul это отдельный список. Проблема в повторяющихся ключах внутри одного списка: React не может различить конфликтующие элементы, и при обновлении списка может их продублировать или потерять.
Когда один пункт рендерит несколько соседних элементов, например <dt> и <dd>, оберните их в <Fragment key={item.id}>. Короткий синтаксис <> ключ не принимает.
Предупреждение «unique key prop»
Если вы не укажете ключ, во время разработки React выведет в консоль браузера следующее:
Each child in a list should have a unique "key" prop.
Check the render method of `App`.
Вторая строка называет компонент, рендер которого создал список, и подсказывает, какой map исправить. Два соседа с одинаковым ключом получают другое сообщение, начинающееся с "Encountered two children with the same key". Превью на этой странице работает как продакшен-сборка, которая пропускает эту проверку, поэтому здесь вы не увидите предупреждения, даже если удалите key. Всё равно удалите его в первом примере: список продолжит рендериться, потому что отсутствующий ключ это риск некорректной работы, а не падение. Чтобы исправить предупреждение, добавьте стабильный ключ элементу, возвращаемому из map; key={index} заглушает его, но возвращает ошибку, показанную выше.
Также учтите, что key не передаётся в ваш компонент. Внутри FruitRow значение props.key равно undefined. Если компоненту нужен ID, передайте его второй раз как обычный пропс: <FruitRow key={f.id} id={f.id} />.
Часто задаваемые вопросы
Зачем React нужен key у элементов списка?
Ключи говорят React, какой элемент какой между рендерами. Когда массив меняется, React сопоставляет старые и новые элементы по ключу и поэтому может сохранить состояние и узел DOM каждого элемента вместе с правильными данными, а не угадывать по позиции.
Можно ли использовать индекс массива как ключ?
Только для списка, порядок которого никогда не меняется и в середину которого никогда не вставляют и не удаляют элементы. Если элементы могут перемещаться, ключ-индекс привязывает состояние к позиции, и набранный текст, флажки и фокус оказываются не у того элемента.
Что использовать как ключ?
ID, который принадлежит данным: ID из базы данных, артикул товара, имя пользователя. Для элементов, созданных в браузере, сгенерируйте ID один раз при создании элемента (счётчик или crypto.randomUUID()), но никогда во время рендера.
Должны ли ключи быть уникальными во всём приложении?
Нет. Ключи должны быть уникальными только среди соседей в одном списке. Два разных списка могут использовать одни и те же ключи.
Может ли компонент прочитать свой ключ?
Нет. key использует React, и в компонент как пропс он не передаётся. Если компоненту нужен ID, передайте его ещё раз под другим именем, например id={item.id}.
Как исправить "Each child in a list should have a unique key prop"?
Добавьте key внешнему элементу, который вы возвращаете из map, используя стабильный ID элемента. Если этот элемент фрагмент, используйте <Fragment key={id}>, потому что <> не принимает ключ.