Рендер списков

Часто возникает необходимость отображения ряда схожих компонентов на основе набора данных. Для этого можно воспользоваться методами массивов JavaScript для манипулирования массивом данных. На этой странице вы будете использовать filter() и map() с React для фильтрации и преобразования массива данных в массив компонентов.

Вы узнаете

  • Как рендерить компоненты из массива, используя map()
  • Как рендерить только определенные компоненты, используя filter()
  • Когда и зачем использовать React-ключи

Рендер данных из массивов

Предположим, у вас есть список контента.

<ul>
<li>Креола Кэтрин Джонсон (Creola Katherine Johnson): математик</li>
<li>Марио Молина (Mario José Molina-Pasquel Henríquez): химик</li>
<li>Мухаммад Абдус Салам (Mohammad Abdus Salam): физик</li>
<li>Перси Джулиан (Percy Lavon Julian): химик</li>
<li>Субраманьян Чандрасекар (Subrahmanyan Chandrasekhar): астрофизик</li>
</ul>

Единственная разница между этими элементами списка - их содержимое, их данные. При построении интерфейсов часто нужно показывать несколько экземпляров одного и того же компонента, используя различные данные: от списков комментариев до галерей профилей. В таких ситуациях вы можете хранить эти данные в объектах и массивах JavaScript и использовать методы, такие как map() и filter(), чтобы рендерить списки компонентов с данными из них.

Вот короткий пример того, как сгенерировать список элементов из массива:

  1. Переместите данные в массив:
const people = [
'Креола Кэтрин Джонсон (Creola Katherine Johnson): математик',
'Марио Молина (Mario José Molina-Pasquel Henríquez): химик',
'Мухаммад Абдус Салам (Mohammad Abdus Salam): физик',
'Перси Джулиан (Percy Lavon Julian): химик',
'Субраманьян Чандрасекар (Subrahmanyan Chandrasekhar): астрофизик'
];
  1. Преобразуйте элементы массива people в новый массив JSX-узлов, listItems, используя метод map():
const listItems = people.map(person => <li>{person}</li>);
  1. Верните listItems из вашего компонента, обернув их в тег <ul>:
return <ul>{listItems}</ul>;

Вот что должно получиться в итоге:

const people = [
  'Креола Кэтрин Джонсон (Creola Katherine Johnson): математик',
  'Марио Молина (Mario José Molina-Pasquel Henríquez): химик',
  'Мухаммад Абдус Салам (Mohammad Abdus Salam): физик',
  'Перси Джулиан (Percy Lavon Julian): химик',
  'Субраманьян Чандрасекар (Subrahmanyan Chandrasekhar): астрофизик'
];

export default function List() {
  const listItems = people.map(person =>
    <li>{person}</li>
  );
  return <ul>{listItems}</ul>;
}

Обратите внимание, что в консоли песочницы отображается ошибка:

Console
Warning: Each child in a list should have a unique “key” prop.

Вы узнаете о том, как исправить эту ошибку позже на этой странице. Прежде чем перейти к этому, давайте добавим некоторую структуру к данным.

Фильтрация массивов элементов

Структуру этих данных можно улучшить.

const people = [{
id: 0,
name: 'Креола Кэтрин Джонсон (Creola Katherine Johnson)',
profession: 'математик',
}, {
id: 1,
name: 'Марио Молина (Mario José Molina-Pasquel Henríquez)',
profession: 'химик',
}, {
id: 2,
name: 'Мухаммад Абдус Салам (Mohammad Abdus Salam)',
profession: 'физик',
}, {
id: 3,
name: 'Перси Джулиан (Percy Lavon Julian)',
profession: 'химик',
}, {
id: 4,
name: 'Субраманьян Чандрасекар (Subrahmanyan Chandrasekhar)',
profession: 'астрофизик',
}];

Допустим, вам нужен способ отображать только людей, чья профессия 'химик'. Вы можете использовать метод JavaScript filter() чтобы вернуть только этих людей. Этот метод принимает массив элементов, пропускает их через «тест» (функция, которая возвращает true или false) и возвращает новый массив только из тех элементов, которые прошли тест (вернули true).

В нашем случае мы хотим отобразить только те элементы, где profession является 'химик'. «Тест» для этого выглядит так: (person) => person.profession === 'химик'. Вот как собрать все воедино:

  1. Создайте новый массив только из людей с профессией 'chemist', вызвав filter() на people для фильтрации по person.profession === 'химик':
const chemists = people.filter(person =>
person.profession === 'химик'
);
  1. Теперь преобразуйте элементы массива chemists:
const listItems = chemists.map(person =>
<li>
<img
src={getImageUrl(person)}
alt={person.name}
/>
<p>
<b>{person.name}:</b>
{' ' + person.profession}.
Достижение: {person.accomplishment}
</p>
</li>
);
  1. Наконец, верните listItems из вашего компонента:
return <ul>{listItems}</ul>;
import { people } from './data.js';
import { getImageUrl } from './utils.js';

export default function List() {
  const chemists = people.filter(person =>
    person.profession === 'химик'
  );
  const listItems = chemists.map(person =>
    <li>
      <img
        src={getImageUrl(person)}
        alt={person.name}
      />
      <p>
        <b>{person.name}:</b>
        {' ' + person.profession}. 
        Достижение:  {person.accomplishment}
      </p>
    </li>
  );
  return <ul>{listItems}</ul>;
}

Pitfall

Стрелочные функции неявно возвращают результат выражения сразу после =>, поэтому использовать ключевое слово return не нужно:

const listItems = chemists.map(person =>
<li>...</li> // Неявный возврат!
);

Однако, вы должны явно вызвать return, если после => следует фигурная скобка!

const listItems = chemists.map(person => { // Фигурная скобка
return <li>...</li>;
});

Стрелочные функции, содержащие => { считаются функциями с “блочной формой”. Они позволяют писать более одной строки кода, но при этом необходимо явно вызвать return. Если вы забудете это сделать, функция ничего не вернет!

Сохранение порядка элементов списка с помощью key

Заметьте, что все песочницы выше показывают ошибку в консоли:

Console
Warning: Each child in a list should have a unique “key” prop.

Чтобы решить эту ошибку необходимо присвоить каждому элементу массива ключ (key) — строку или число, которое уникально отличает данный элемент среди других элементов этого массива:

<li key={person.id}>...</li>

Note

JSX-элементы, созданные внутри map() всегда должны иметь ключи!

Ключи позволяют React узнать к какому элементу массива соответствует каждый компонент, чтобы позже сопоставить их. Это важно, если элементы массива могут перемещаться (например, из-за сортировки), добавляться или удаляться. Хорошо выбранный ключ помогает React понять, какое именно изменение произошло, и правильно обновить DOM дерево.

Вместо генерации ключей на лету, вы должны включать их в свои данные:

export const people = [{
  id: 0, // Используется в JSX в качестве ключа
  name: 'Креола Кэтрин Джонсон (Creola Katherine Johnson)',
  profession: 'математик',
  accomplishment: 'расчёты для космических полетов',
  imageId: 'MK3eW3A'
}, {
  id: 1, // Используется в JSX в качестве ключа
  name: 'Марио Молина (Mario José Molina-Pasquel Henríquez)',
  profession: 'химик',
  accomplishment: 'обнаружение дыр в озоновом слое',
  imageId: 'mynHUSa'
}, {
  id: 2, // Используется в JSX в качестве ключа
  name: 'Мухаммад Абдус Салам (Mohammad Abdus Salam)',
  profession: 'физик',
  accomplishment: 'открытие теории электромагнетизма',
  imageId: 'bE7W1ji'
}, {
  id: 3, // Используется в JSX в качестве ключа
  name: 'Перси Джулиан (Percy Lavon Julian)',
  profession: 'химик',
  accomplishment: 'изобретение препаратов с кортизоном, стероидов и противозачаточных таблеток',
  imageId: 'IOjWm71'
}, {
  id: 4, // Используется в JSX в качестве ключа
  name: 'Субраманьян Чандрасекар (Subrahmanyan Chandrasekhar)',
  profession: 'астрофизик',
  accomplishment: 'расчёт массы белого карлика',
  imageId: 'lrWQx8l'
}];

Deep Dive

Отображение нескольких DOM-узлов для каждого элемента списка

Как поступить, если каждый элемент должен отображать не один, а несколько DOM-узлов?

Краткий синтаксис <>...</> фрагмента не позволяет передавать ключ, поэтому нужно либо объединить их в один <div>, либо использовать чуть более длинный и более явный <Fragment>:

import { Fragment } from 'react';

// ...

const listItems = people.map(person =>
<Fragment key={person.id}>
<h1>{person.name}</h1>
<p>{person.bio}</p>
</Fragment>
);

Фрагменты исчезают из DOM, поэтому это приведет к плоскому списку элементов <h1>, <p>, <h1>, <p>, и т.д.

Откуда взять ключ

Разные источники данных предоставляют разные источники для ключей:

  • Данные из базы данных: Если ваши данные приходят с базы данных, то вы можете использовать ключи/ID с базы данных, которые по своей природе уникальны.

  • Локальные данные: Если ваши данные генерируются и хранятся локально (к примеру, заметки в приложении для ведений заметок), используйте инкрементный счетчик, crypto.randomUUID() или пакет uuid при создании элементов.

Правила ключей

  • Ключи должны быть уникальны среди своих соседних элементов. Однако, можно использовать одинаковые ключи для JSX-узлов в разных массивах.
  • Ключи не должны меняться, так как это лишает их смысла! Не генерируйте их во время рендеринга.

Почему React нужны ключи?

Представьте что у файлов на вашем рабочем столе не было бы имен. Взамен, вы бы ссылались на них по их порядку — первый файл, второй файл, и т.д. Возможно к этому и можно привыкнуть, но когда вы удалите какой-либо файл, порядок изменится и все станет запутанным. Второй файл станет первым, третий файл станет вторым, и т. д.

Названия файлов в папке и JSX ключи в массиве имеют схожую цель. Они позволяют нам отличать элементы от их других элементов в массиве. А хорошо выбранный ключ предоставляет больше информации, чем позиция в массиве. Даже если позиция изменится из-за смены порядка, key позволит React идентифицировать элемент на протяжении всего существования элемента.

Pitfall

Возможно вам захочется использовать индекс элемента в массиве в качестве ключа. В действительности, это то, что React будет использовать, если вы не укажете key. Но порядок, в котором вы рендерите элементы, может поменяться со временем, если какой-либо элемент будет вставлен, удален или если массив будет переупорядочен. Индекс в качестве ключа часто приводит к коварным и сбивающим с толку ошибкам.

Аналогично, не генерируйте ключи на лету, например, с помощью key={Math.random()}. Это приведет к тому, что ключи никогда не будут совпадать между рендерами, что приведет к пересозданию всех ваших компонентов и DOM при каждом рендере. Это не только медленно, но также приведет к потере любых данных введённых пользователем внутри элементов списка. Вместо этого используйте стабильный ID, основанный на данных.

Заметьте, что ваши компоненты не получат key в качестве пропа. Он используется только как подсказка для React. Если ваш компонент нуждается в ID, вы должны передать его как отдельный проп: <Profile key={id} userId={id} />..

Recap

На этой странице вы узнали:

  • Как перенести данные из компонентов в структуры данных, такие как массивы и объекты.
  • Как создавать коллекции схожих компонентов с помощью JavaScript map().
  • Как создавать массивы отфильтрованных элементов с помощью JavaScript filter().
  • Зачем и как присваивать ключ каждому компоненту в коллекции, чтобы React мог отслеживать изменения каждого из них.

Challenge 1 of 4:
Разделение списка на два

Этот пример показывает список всех людей в people.

Поменяйте код так, чтобы он показывал два списка один за другим: Химики и Все остальные. Как и раньше, вы можете определить, является ли человек химиком, проверив person.profession === 'химик'.

import { people } from './data.js';
import { getImageUrl } from './utils.js';

export default function List() {
  const listItems = people.map(person =>
    <li key={person.id}>
      <img
        src={getImageUrl(person)}
        alt={person.name}
      />
      <p>
        <b>{person.name}:</b>
        {' ' + person.profession}.
        Достижение: {person.accomplishment}
      </p>
    </li>
  );
  return (
    <article>
      <h1>Ученые</h1>
      <ul>{listItems}</ul>
    </article>
  );
}