Рендеринг списков¶
Часто требуется отобразить несколько одинаковых компонентов из набора данных. Для работы с массивом данных можно использовать методы JavaScript массива. На этой странице вы будете использовать filter()
и map()
в React для фильтрации и преобразования массива данных в массив компонентов.
Вы узнаете
- Как выводить компоненты из массива с помощью JavaScript
map()
. - Как отобразить только определенные компоненты с помощью JavaScript
filter()
. - Когда и зачем использовать ключи React
Рендеринг данных из массивов¶
Допустим, у вас есть список содержимого.
1 2 3 4 5 6 7 |
|
Единственное различие между этими элементами списка — это их содержимое, их данные. При построении интерфейсов часто требуется отображать несколько экземпляров одного и того же компонента, используя разные данные: от списков комментариев до галерей изображений профиля. В таких ситуациях вы можете хранить эти данные в объектах и массивах JavaScript и использовать такие методы, как map()
и filter()
для вывода списков компонентов на их основе.
Вот краткий пример того, как сформировать список элементов из массива:
1. Переместите данные в массив:
1 2 3 4 5 6 7 |
|
2. Мап членов people
в новый массив узлов JSX, listItems
:
1 |
|
3. Возвращает listItems
из вашего компонента, обернутого в <ul>
:
1 |
|
Вот результат:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
|
1 2 3 4 5 6 |
|
Обратите внимание, что в песочнице выше отображается консольная ошибка:
Console
Warning: Each child in a list should have a unique “key” prop.
Как исправить эту ошибку, вы узнаете позже на этой странице. Прежде чем мы приступим к этому, давайте добавим немного структуры в ваши данные.
Фильтрация массивов элементов¶
Эти данные можно структурировать еще больше.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
|
Допустим, вам нужно показать только людей, чья профессия — chemist
. Вы можете использовать метод JavaScript filter()
, чтобы вернуть только таких людей. Этот метод принимает массив элементов, пропускает их через "тест" (функцию, которая возвращает true
или false
) и возвращает новый массив только тех элементов, которые прошли тест (вернули true
).
Вам нужны только те элементы, где profession
— "chemist"
. Функция "test" для этого выглядит как (person) => person.profession === 'chemist'
. Вот как это можно сделать:
1. Создаем новый массив только "химиков", chemists
, вызывая filter()
на people
, фильтруя по person.profession === 'chemist'
:
1 2 3 |
|
2. Теперь составьте карту над химиками
:
1 2 3 4 5 6 7 8 9 10 |
|
3. И наконец, возвратите список listItems
из вашего компонента:
1 |
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 |
|
1 2 3 4 5 |
|
Внимание
Стрелочные функции неявно возвращают выражение сразу после =>
, поэтому оператор возврата
не нужен:
1 2 3 |
|
Однако вы должны явно написать return
, если за вашим =>
следует {
фигурная скобка!.
1 2 3 4 |
|
Стрелочные функции, содержащие => {
, как говорят, имеют "тело блока". Они позволяют вам написать больше, чем одну строку кода, но вы должны сами написать оператор возврата
. Если вы забудете об этом, ничего не будет возвращено!
Упорядочивание элементов списка по key
¶
Обратите внимание, что все приведенные выше песочницы выдают ошибку в консоли:
Console
Warning: Each child in a list should have a unique “key” prop.
Вам нужно дать каждому элементу массива key
— строку или число, которое уникально идентифицирует его среди других элементов этого массива:
1 |
|
Ключи внутри map()
JSX-элементы непосредственно внутри вызова map()
всегда нуждаются в ключах!
Ключи указывают React, какому элементу массива соответствует каждый компонент, чтобы он мог сопоставить их позже. Это становится важным, если элементы массива могут перемещаться (например, из-за сортировки), вставляться или удаляться. Хорошо подобранный key
помогает React понять, что именно произошло, и сделать правильные обновления в DOM-дереве.
Вместо того чтобы генерировать ключи "на лету", вы должны включать их в свои данные:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 |
|
1 2 3 4 5 |
|
Отображение нескольких узлов DOM для каждого элемента списка
Что делать, если каждый элемент должен отображать не один, а несколько узлов DOM?
Короткий синтаксис <>...</>
Fragment не позволит вам передать ключ, поэтому вам нужно либо сгруппировать их в один <div>
, либо использовать немного более длинный и более явный синтаксис <Fragment>
:
1 2 3 4 5 6 7 8 9 10 |
|
Фрагменты исчезают из DOM, поэтому будет получен плоский список <h1>
, <p>
, <h1>
, <p>
и так далее.
Где взять ваш key
¶
Различные источники данных предоставляют различные источники ключей:
- Данные из базы данных: Если ваши данные поступают из базы данных, вы можете использовать ключи/идентификаторы базы данных, которые уникальны по своей природе.
- Локально генерируемые данные: Если ваши данные генерируются и сохраняются локально (например, заметки в приложении для ведения записей), при создании элементов используйте инкрементирующий счетчик,
crypto.randomUUID()
или пакет типаuuid
.
Правила ключей¶
- Ключи должны быть уникальными среди братьев и сестер. Однако, можно использовать одинаковые ключи для JSX-узлов в разных массивах.
- Ключи не должны меняться, иначе это противоречит их назначению! Не генерируйте их во время рендеринга.
Зачем React нужны ключи?¶
Представьте, что файлы на вашем рабочем столе не имеют имен. Вместо этого вы обращаетесь к ним по порядку — первый файл, второй файл и так далее. К этому можно было бы привыкнуть, но когда вы удалите файл, все запутается. Второй файл станет первым, третий — вторым и так далее.
Имена файлов в папке и JSX-ключи в массиве служат аналогичной цели. Они позволяют нам однозначно идентифицировать элемент между его родственниками. Хорошо подобранный ключ предоставляет больше информации, чем позиция в массиве. Даже если позиция меняется из-за переупорядочивания, key
позволяет React идентифицировать элемент на протяжении всего его существования.
Внимание
У вас может возникнуть соблазн использовать индекс элемента в массиве в качестве ключа. На самом деле, именно это будет использовать React, если вы вообще не укажете key
. Но порядок, в котором вы отображаете элементы, будет меняться со временем, если элемент будет вставлен, удален или массив будет переупорядочен. Индекс в качестве ключа часто приводит к тонким и запутанным ошибкам.
Аналогично, не генерируйте ключи на лету, например, с помощью key={Math.random()}
. Это приведет к тому, что ключи никогда не будут совпадать между рендерами, что приведет к тому, что все ваши компоненты и DOM будут каждый раз создаваться заново. Это не только медленно, но и приведет к потере пользовательского ввода внутри элементов списка. Вместо этого используйте стабильный идентификатор, основанный на данных.
Обратите внимание, что ваши компоненты не будут получать key
в качестве пропса. Он используется только в качестве подсказки самим React. Если вашему компоненту нужен ID, вы должны передать его как отдельный пропс: <Profile key={id} userId={id} />
.
Итоги
На этой странице вы узнали:
- Как перемещать данные из компонентов в структуры данных, такие как массивы и объекты.
- Как генерировать наборы похожих компонентов с помощью функции JavaScript
map()
. - Как создавать массивы отфильтрованных элементов с помощью функции JavaScript
filter()
. - Зачем и как устанавливать
key
для каждого компонента в коллекции, чтобы React мог отслеживать каждый из них, даже если их положение или данные меняются.
Задачи¶
1. Разделение списка на две части¶
В этом примере показан список всех людей.
Измените его, чтобы последовательно вывести два отдельных списка: Химики и Все остальные. Как и ранее, вы можете определить, является ли человек химиком, проверив, что person.profession === 'chemist'
.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 |
|
1 2 3 4 5 |
|
Показать решение
Вы можете использовать filter()
дважды, создавая два отдельных массива, а затем map
над обоими массивами:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 |
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 |
|
1 2 3 4 5 |
|
В данном решении вызовы map
размещены непосредственно в родительских элементах <ul>
, но вы можете ввести для них переменные, если считаете это более читабельным.
Между отображаемыми списками все еще есть некоторое дублирование. Можно пойти дальше и извлечь повторяющиеся части в компонент <ListSection>
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 |
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 |
|
1 2 3 4 5 |
|
Очень внимательный читатель может заметить, что при двух вызовах filter
мы проверяем профессию каждого человека дважды. Проверка свойства происходит очень быстро, поэтому в данном примере это нормально. Если бы ваша логика была более дорогой, вы могли бы заменить вызовы filter
циклом, который вручную строит массивы и проверяет каждого человека один раз.
На самом деле, если people
никогда не меняется, вы можете вынести этот код за пределы вашего компонента. С точки зрения React, все, что имеет значение, это то, что в конечном итоге вы даете ему массив JSX-узлов. Ему все равно, как вы создадите этот массив:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 |
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 |
|
1 2 3 4 5 |
|
2. Вложенные списки в одном компоненте¶
Создайте список рецептов из этого массива! Для каждого рецепта в массиве выведите его название в виде <h2>
и список ингредиентов в виде <ul>
.
Это потребует вложения двух различных вызовов map
.
1 2 3 4 5 6 7 8 9 |
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 |
|
Показать подсказку
Это потребует вложения двух различных вызовов map
.
Показать решение
Вот один из способов, которым вы можете воспользоваться:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 |
|
Каждый из recipes
уже содержит поле id
, поэтому именно его использует внешний цикл для своего key
. Нет никакого идентификатора, который можно было бы использовать для перебора ингредиентов. Однако разумно предположить, что один и тот же ингредиент не будет указан дважды в одном рецепте, поэтому его название может служить в качестве key
. В качестве альтернативы можно изменить структуру данных, добавив идентификаторы, или использовать индекс в качестве key
(с оговоркой, что вы не сможете безопасно переупорядочить ингредиенты).
3. Извлечение компонента элемента списка¶
Этот компонент RecipeList
содержит два вложенных вызова map
. Чтобы упростить его, извлеките из него компонент Recipe
, который будет принимать пропсы id
, name
и ingredients
. Где вы разместите внешний key
и почему?
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 |
|
Показать решение
Вы можете скопировать-вставить JSX из внешней map
в новый компонент Recipe
и вернуть этот JSX. Затем вы можете изменить recipe.name
на name
, recipe.id
на id
, и так далее, и передать их в качестве пропсов в Recipe
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 |
|
Здесь <Recipe {...recipe} key={recipe.id} />
— это синтаксическое сокращение, говорящее "передать все свойства объекта recipe
в качестве пропсов компоненту Recipe
". Вы также можете записать каждый пропс в явном виде: <Recipe id={recipe.id} name={recipe.name} ingredients={recipe.ingredients} key={recipe.id} />
.
Обратите внимание, что key
указывается на самом <Recipe>
, а не на корне <div>
, возвращаемом из Recipe
. Это потому, что этот key
нужен непосредственно в контексте окружающего массива. Раньше у вас был массив <div>
, и каждый из них нуждался в key
, а теперь у вас есть массив <Recipe>
. Другими словами, когда вы извлекаете компонент, не забудьте оставить key
вне JSX, который вы копируете и вставляете.
4. Список с разделителем¶
Этот пример отображает знаменитое хайку Кацусики Хокусая, каждая строка которого обернута в тег <p>
. Ваша задача — вставить разделитель <hr />
между каждым абзацем. Ваша результирующая структура должна выглядеть следующим образом:
1 2 3 4 5 6 7 |
|
В хайку всего три строки, но ваше решение должно работать с любым количеством строк. Обратите внимание, что элементы <hr />
появляются только между элементами <p>
, а не в начале или конце!
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
|
Это редкий случай, когда индекс в качестве ключа допустим, потому что строки стихотворения никогда не будут перестраиваться.
Показать подсказку
Вам нужно либо преобразовать map
в ручной цикл, либо использовать фрагмент.
Показать решение
Вы можете написать ручной цикл, вставляя <hr />
и <p>...</p>
в выходной массив по мере выполнения:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
|
Использование исходного индекса строки в качестве key
больше не работает, поскольку каждый разделитель и абзац теперь находятся в одном массиве. Однако вы можете дать каждому из них отдельный ключ, используя суффикс, например, key={i + '-text'}
.
В качестве альтернативы можно вывести коллекцию фрагментов, содержащих <hr />
и <p>...</p>
. Однако синтаксис сокращения <>...</>
не поддерживает передачу ключей, поэтому вам придется явно написать <Fragment>
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
|
Помните, что фрагменты (часто записываемые как <>...</>
) позволяют вам группировать узлы JSX без добавления лишних <div>
!
Источник — https://react.dev/learn/rendering-lists