Списки рендерингу

Вам часто буде потрібно відображати декілька схожих компонентів з набору даних. Для маніпулювання масивом даних можна скористатися методами масиву JavaScript. На цій сторінці ви будете використовувати filter() та map() з React для фільтрації та перетворення масиву даних на масив компонентів.

  • Як рендерити компоненти з масиву за допомогою JavaScript map()
  • Як відобразити лише певні компоненти за допомогою JavaScript filter()
  • Коли і навіщо використовувати ключі React

Відображення даних з масивів

Скажімо, що у вас є список вмісту.

<ul>
  <li>Creola Katherine Johnson: mathematician</li>
  <li>Mario José Molina-Pasquel Henríquez: chemist</li>
  <li>Mohammad Abdus Salam: physicist</li>
  <li>Percy Lavon Julian: chemist</li>
  <li>Subrahmanyan Chandrasekhar: astrophysicist</li>
</ul>

Єдина відмінність між цими елементами списку - це їхній вміст, їхні дані. При створенні інтерфейсів вам часто потрібно буде показувати кілька екземплярів одного і того ж компонента з різними даними: від списків коментарів до галерей зображень профілю. У таких ситуаціях ви можете зберігати ці дані в об'єктах і масивах JavaScript і використовувати методи на кшталт map() і filter() для відображення списків компонентів з них.

Короткий приклад формування списку елементів з масиву:

  1. Перемістити дані у масив:
const people = [
  'Creola Katherine Johnson: mathematician',
  'Mario José Molina-Pasquel Henríquez: chemist',
  'Mohammad Abdus Salam: physicist',
  'Percy Lavon Julian: chemist',
  'Subrahmanyan Chandrasekhar: astrophysicist'
];
  1. Зобразити членів people у новий масив вузлів JSX, listItems:
const listItems = people.map(person => <li>{person}</li>);
  1. Поверніть listItems з вашого компонента, обгорнутого в <ul>:
return <ul>{listItems}</ul>;

Ось результат:

const people = [
  'Creola Katherine Johnson: mathematician',
  'Mario José Molina-Pasquel Henríquez: chemist',
  'Mohammad Abdus Salam: physicist',
  'Percy Lavon Julian: chemist',
  'Subrahmanyan Chandrasekhar: astrophysicist'
];

export default function List() {
  const listItems = people.map(person =>
    <li>{person}</li>
  );
  return <ul>{listItems}</ul>;
}
li { margin-bottom: 10px; }

Зауважте, що у наведеній вище пісочниці з'являється помилка консолі:

Warning: Кожен дочірній елемент у списку повинен мати унікальний "ключовий" проп.

Ви дізнаєтеся, як виправити цю помилку пізніше на цій сторінці. Перш ніж ми перейдемо до цього, давайте додамо деяку структуру до ваших даних.

Фільтрування масивів елементів

Ці дані можна структурувати ще більше.

const people = [{
  id: 0,
  name: 'Creola Katherine Johnson',
  profession: 'mathematician',
}, {
  id: 1,
  name: 'Mario José Molina-Pasquel Henríquez',
  profession: 'chemist',
}, {
  id: 2,
  name: 'Mohammad Abdus Salam',
  profession: 'physicist',
}, {
  name: 'Percy Lavon Julian',
  profession: 'chemist',  
}, {
  name: 'Subrahmanyan Chandrasekhar',
  profession: 'astrophysicist',
}];

Припустимо, вам потрібен спосіб показувати лише людей, професія яких 'chemist'. Ви можете скористатися методом filter() у JavaScript, щоб повернути лише цих людей. Цей метод приймає масив елементів, пропускає їх через "тест" (функцію, яка повертає true або false) і повертає новий масив лише тих елементів, які пройшли тест (повертається true).

Вам потрібні лише ті позиції, де професія дорівнює 'chemist'. Функція "test" для цього має вигляд (person) => person.profession === 'chemist'. Ось як це можна виразити:

  1. Створіть новий масив лише "хіміків", chemists, викликавши filter() на people фільтрацію за person.profession === 'chemist':
const chemists = people.filter(person =>
  person.profession === 'chemist'
);
  1. Тепер map над chemists:
const listItems = chemists.map(person =>
  <li>
     <img
       src={getImageUrl(person)}
       alt={person.name}
     />
     <p>
       <b>{person.name}:</b>
       {' ' + person.profession + ' '}
       known for {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 === 'chemist'
  );
  const listItems = chemists.map(person =>
    <li>
      <img
        src={getImageUrl(person)}
        alt={person.name}
      />
      <p>
        <b>{person.name}:</b>
        {' ' + person.profession + ' '}
        known for {person.accomplishment}
      </p>
    </li>
  );
  return <ul>{listItems}</ul>;
}
export const people = [{
  id: 0,
  name: 'Creola Katherine Johnson',
  profession: 'mathematician',
  accomplishment: 'spaceflight calculations',
  imageId: 'MK3eW3A'
}, {
  id: 1,
  name: 'Mario José Molina-Pasquel Henríquez',
  profession: 'chemist',
  accomplishment: 'discovery of Arctic ozone hole',
  imageId: 'mynHUSa'
}, {
  id: 2,
  name: 'Mohammad Abdus Salam',
  profession: 'physicist',
  accomplishment: 'electromagnetism theory',
  imageId: 'bE7W1ji'
}, {
  id: 3,
  name: 'Percy Lavon Julian',
  profession: 'chemist',
  accomplishment: 'pioneering cortisone drugs, steroids and birth control pills',
  imageId: 'IOjWm71'
}, {
  id: 4,
  name: 'Subrahmanyan Chandrasekhar',
  profession: 'astrophysicist',
  accomplishment: 'white dwarf star mass calculations',
  imageId: 'lrWQx8l'
}];
export function getImageUrl(person) {
  return (
    'https://i.imgur.com/' +
    person.imageId +
    's.jpg'
  );
}
ul { list-style-type: none; padding: 0px 10px; }
li { 
  margin-bottom: 10px; 
  display: grid; 
  grid-template-columns: auto 1fr;
  gap: 20px;
  align-items: center;
}
img { width: 100px; height: 100px; border-radius: 50%; }

Функції зі стрілками неявно повертають вираз одразу після =>, тому вам не потрібен оператор return:

const listItems = chemists.map(person =>
  <li>...</li> // Implicit return!
);

Однак, ви повинні явно написати return, якщо після вашого => слідує { фігурна дужка!

const listItems = chemists.map(person => { // Curly brace
  return <li>...</li>;
});

Функції-стрілки, що містять => {, вважаються такими, що мають "тіло блоку". Вони дозволяють написати більше одного рядка коду, але ви повинні самі написати інструкцію return. Якщо ви забудете його, нічого не буде повернуто!

Впорядкування елементів списку за допомогою ключа

Зверніть увагу, що всі наведені вище пісочниці показують помилку у консолі:

Warning: Кожен дочірній елемент у списку повинен мати унікальний "ключовий" проп.

Кожному елементу масиву потрібно надати ключ - рядок або число, яке однозначно ідентифікує його серед інших елементів цього масиву:

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

JSX-елементи безпосередньо всередині виклику map() завжди потребують ключів!

Ключі вказують React, якому елементу масиву відповідає кожен компонент, щоб пізніше він міг їх співставити. Це стає важливим, якщо елементи вашого масиву можуть переміщатися (наприклад, через сортування), вставлятися або видалятися. Вдало підібраний ключ допомагає React зрозуміти, що саме сталося, і зробити правильні оновлення DOM-дерева.

Замість того, щоб генерувати ключі на льоту, вам слід включити їх у ваші дані:

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 + ' '}
          known for {person.accomplishment}
      </p>
    </li>
  );
  return <ul>{listItems}</ul>;
}
export const people = [{
  id: 0, // Used in JSX as a key
  name: 'Creola Katherine Johnson',
  profession: 'mathematician',
  accomplishment: 'spaceflight calculations',
  imageId: 'MK3eW3A'
}, {
  id: 1, // Used in JSX as a key
  name: 'Mario José Molina-Pasquel Henríquez',
  profession: 'chemist',
  accomplishment: 'discovery of Arctic ozone hole',
  imageId: 'mynHUSa'
}, {
  id: 2, // Used in JSX as a key
  name: 'Mohammad Abdus Salam',
  profession: 'physicist',
  accomplishment: 'electromagnetism theory',
  imageId: 'bE7W1ji'
}, {
  id: 3, // Used in JSX as a key
  name: 'Percy Lavon Julian',
  profession: 'chemist',
  accomplishment: 'pioneering cortisone drugs, steroids and birth control pills',
  imageId: 'IOjWm71'
}, {
  id: 4, // Used in JSX as a key
  name: 'Subrahmanyan Chandrasekhar',
  profession: 'astrophysicist',
  accomplishment: 'white dwarf star mass calculations',
  imageId: 'lrWQx8l'
}];
export function getImageUrl(person) {
  return (
    'https://i.imgur.com/' +
    person.imageId +
    's.jpg'
  );
}
ul { list-style-type: none; padding: 0px 10px; }
li { 
  margin-bottom: 10px; 
  display: grid; 
  grid-template-columns: auto 1fr;
  gap: 20px;
  align-items: center;
}
img { width: 100px; height: 100px; border-radius: 50%; }

Відображення декількох 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> і так далі.

Де отримати свій ключ

Різні джерела даних надають різні джерела ключів:

  • Дані з бази даних: Якщо ваші дані надходять з бази даних, ви можете використовувати ключі/ідентифікатори бази даних, які є унікальними за своєю природою.
  • Локально згенеровані дані: Якщо ваші дані генеруються та зберігаються локально (наприклад, нотатки у застосунку для нотаток), використовуйте лічильник з інкрементом crypto.randomUUID() або пакунок на кшталт uuid при створенні елементів.

Правила ключів

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

Навіщо React потрібні ключі?

Уявіть, що файли на вашому робочому столі не мають назв. Замість цього ви б зверталися до них по порядку - перший файл, другий файл і так далі. До цього можна було б звикнути, але щойно ви видалите файл, все стане заплутаним. Другий файл стане першим, третій - другим і так далі.

Імена файлів у теці та JSX-ключі у масиві мають схожу мету. Вони дозволяють нам однозначно ідентифікувати елемент між його братами і сестрами. Вдало підібраний ключ надає більше інформації, ніж позиція у масиві. Навіть якщо позиція змінюється через переупорядкування, ключ дозволяє React ідентифікувати елемент протягом усього його життя.

У вас може виникнути спокуса використовувати індекс елемента в масиві як його ключ. Насправді, це те, що React буде використовувати, якщо ви не вкажете key взагалі. Але порядок, в якому ви рендерите елементи, буде змінюватися з часом, якщо елемент буде вставлено, видалено або якщо масив буде переупорядковано. Використання індексу як ключа часто призводить до витончених і заплутаних вад.

Також не генеруйте ключі на льоту, наприклад, за допомогою key={Math.random()}. Це призведе до того, що ключі ніколи не будуть збігатися між рендерингами, що призведе до того, що всі ваші компоненти і DOM будуть щоразу створюватися заново. Це не тільки повільно, але й призведе до втрати даних, введених користувачем всередині елементів списку. Замість цього використовуйте стабільний ідентифікатор на основі даних.

Зверніть увагу, що ваші компоненти не отримають key як проп. Сам React використовує його лише як підказку. Якщо вашому компоненту потрібен ідентифікатор, ви повинні передати його як окремий проп: <Profile key={id} userId={id} />.

На цій сторінці ви дізналися:

  • Як переміщувати дані з компонентів у структури даних, такі як масиви та об'єкти.
  • Як створити набори подібних компонентів за допомогою JavaScript map().
  • Як створити масиви відфільтрованих елементів за допомогою JavaScript filter().
  • Чому і як встановити ключ для кожного компонента в колекції, щоб React міг відслідковувати кожен з них, навіть якщо їх позиція або дані змінюються.

Поділ списку на дві частини

У цьому прикладі показано список усіх людей.

Змініть його, щоб показати два окремих списки один за одним: Хіміки та Всі інші. Як і раніше, ви можете визначити, чи є людина хіміком, перевіривши, чи person.profession === 'chemist'.

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 + ' '}
        known for {person.accomplishment}
      </p>
    </li>
  );
  return (
    <article>
      <h1>Scientists</h1>
      <ul>{listItems}</ul>
    </article>
  );
}
export const people = [{
  id: 0,
  name: 'Creola Katherine Johnson',
  profession: 'mathematician',
  accomplishment: 'spaceflight calculations',
  imageId: 'MK3eW3A'
}, {
  id: 1,
  name: 'Mario José Molina-Pasquel Henríquez',
  profession: 'chemist',
  accomplishment: 'discovery of Arctic ozone hole',
  imageId: 'mynHUSa'
}, {
  id: 2,
  name: 'Mohammad Abdus Salam',
  profession: 'physicist',
  accomplishment: 'electromagnetism theory',
  imageId: 'bE7W1ji'
}, {
  id: 3,
  name: 'Percy Lavon Julian',
  profession: 'chemist',
  accomplishment: 'pioneering cortisone drugs, steroids and birth control pills',
  imageId: 'IOjWm71'
}, {
  id: 4,
  name: 'Subrahmanyan Chandrasekhar',
  profession: 'astrophysicist',
  accomplishment: 'white dwarf star mass calculations',
  imageId: 'lrWQx8l'
}];
export function getImageUrl(person) {
  return (
    'https://i.imgur.com/' +
    person.imageId +
    's.jpg'
  );
}
ul { list-style-type: none; padding: 0px 10px; }
li {
  margin-bottom: 10px;
  display: grid;
  grid-template-columns: auto 1fr;
  gap: 20px;
  align-items: center;
}
img { width: 100px; height: 100px; border-radius: 50%; }

Ви можете використати filter() двічі, створивши два окремі масиви, а потім накласти на них обидва:

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

export default function List() {
  const chemists = people.filter(person =>
    person.profession === 'chemist'
  );
  const everyoneElse = people.filter(person =>
    person.profession !== 'chemist'
  );
  return (
    <article>
      <h1>Scientists</h1>
      <h2>Chemists</h2>
      <ul>
        {chemists.map(person =>
          <li key={person.id}>
            <img
              src={getImageUrl(person)}
              alt={person.name}
            />
            <p>
              <b>{person.name}:</b>
              {' ' + person.profession + ' '}
              known for {person.accomplishment}
            </p>
          </li>
        )}
      </ul>
      <h2>Everyone Else</h2>
      <ul>
        {everyoneElse.map(person =>
          <li key={person.id}>
            <img
              src={getImageUrl(person)}
              alt={person.name}
            />
            <p>
              <b>{person.name}:</b>
              {' ' + person.profession + ' '}
              known for {person.accomplishment}
            </p>
          </li>
        )}
      </ul>
    </article>
  );
}
export const people = [{
  id: 0,
  name: 'Creola Katherine Johnson',
  profession: 'mathematician',
  accomplishment: 'spaceflight calculations',
  imageId: 'MK3eW3A'
}, {
  id: 1,
  name: 'Mario José Molina-Pasquel Henríquez',
  profession: 'chemist',
  accomplishment: 'discovery of Arctic ozone hole',
  imageId: 'mynHUSa'
}, {
  id: 2,
  name: 'Mohammad Abdus Salam',
  profession: 'physicist',
  accomplishment: 'electromagnetism theory',
  imageId: 'bE7W1ji'
}, {
  id: 3,
  name: 'Percy Lavon Julian',
  profession: 'chemist',
  accomplishment: 'pioneering cortisone drugs, steroids and birth control pills',
  imageId: 'IOjWm71'
}, {
  id: 4,
  name: 'Subrahmanyan Chandrasekhar',
  profession: 'astrophysicist',
  accomplishment: 'white dwarf star mass calculations',
  imageId: 'lrWQx8l'
}];
export function getImageUrl(person) {
  return (
    'https://i.imgur.com/' +
    person.imageId +
    's.jpg'
  );
}
ul { list-style-type: none; padding: 0px 10px; }
li {
  margin-bottom: 10px;
  display: grid;
  grid-template-columns: auto 1fr;
  gap: 20px;
  align-items: center;
}
img { width: 100px; height: 100px; border-radius: 50%; }

У цьому рішенні виклики map розміщено безпосередньо у батьківських елементах <ul>, але ви можете ввести для них змінні, якщо вважаєте це більш читабельним.

У виведених списках все ще спостерігається деяке дублювання. Ви можете піти далі і витягти частини, що повторюються, у компонент <ListSection>:

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

function ListSection({ title, people }) {
  return (
    <>
      <h2>{title}</h2>
      <ul>
        {people.map(person =>
          <li key={person.id}>
            <img
              src={getImageUrl(person)}
              alt={person.name}
            />
            <p>
              <b>{person.name}:</b>
              {' ' + person.profession + ' '}
              known for {person.accomplishment}
            </p>
          </li>
        )}
      </ul>
    </>
  );
}

export default function List() {
  const chemists = people.filter(person =>
    person.profession === 'chemist'
  );
  const everyoneElse = people.filter(person =>
    person.profession !== 'chemist'
  );
  return (
    <article>
      <h1>Scientists</h1>
      <ListSection
        title="Chemists"
        people={chemists}
      />
      <ListSection
        title="Everyone Else"
        people={everyoneElse}
      />
    </article>
  );
}
export const people = [{
  id: 0,
  name: 'Creola Katherine Johnson',
  profession: 'mathematician',
  accomplishment: 'spaceflight calculations',
  imageId: 'MK3eW3A'
}, {
  id: 1,
  name: 'Mario José Molina-Pasquel Henríquez',
  profession: 'chemist',
  accomplishment: 'discovery of Arctic ozone hole',
  imageId: 'mynHUSa'
}, {
  id: 2,
  name: 'Mohammad Abdus Salam',
  profession: 'physicist',
  accomplishment: 'electromagnetism theory',
  imageId: 'bE7W1ji'
}, {
  id: 3,
  name: 'Percy Lavon Julian',
  profession: 'chemist',
  accomplishment: 'pioneering cortisone drugs, steroids and birth control pills',
  imageId: 'IOjWm71'
}, {
  id: 4,
  name: 'Subrahmanyan Chandrasekhar',
  profession: 'astrophysicist',
  accomplishment: 'white dwarf star mass calculations',
  imageId: 'lrWQx8l'
}];
export function getImageUrl(person) {
  return (
    'https://i.imgur.com/' +
    person.imageId +
    's.jpg'
  );
}
ul { list-style-type: none; padding: 0px 10px; }
li {
  margin-bottom: 10px;
  display: grid;
  grid-template-columns: auto 1fr;
  gap: 20px;
  align-items: center;
}
img { width: 100px; height: 100px; border-radius: 50%; }

Уважний читач може помітити, що за допомогою двох викликів фільтру ми двічі перевіряємо професію кожної людини. Перевірка властивості відбувається дуже швидко, тому в цьому прикладі це нормально. Якщо ваша логіка була б складнішою, ви могли б замінити виклики filter циклом, який вручну створює масиви і перевіряє кожну особу один раз.

Насправді, якщо люди ніколи не змінюються, ви можете винести цей код з вашого компонента. З точки зору React, все, що має значення - це те, що ви передаєте йому масив JSX-вузлів в кінці. Йому не важливо, як ви отримаєте цей масив:

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

let chemists = [];
let everyoneElse = [];
people.forEach(person => {
  if (person.profession === 'chemist') {
    chemists.push(person);
  } else {
    everyoneElse.push(person);
  }
});

function ListSection({ title, people }) {
  return (
    <>
      <h2>{title}</h2>
      <ul>
        {people.map(person =>
          <li key={person.id}>
            <img
              src={getImageUrl(person)}
              alt={person.name}
            />
            <p>
              <b>{person.name}:</b>
              {' ' + person.profession + ' '}
              known for {person.accomplishment}
            </p>
          </li>
        )}
      </ul>
    </>
  );
}

export default function List() {
  return (
    <article>
      <h1>Scientists</h1>
      <ListSection
        title="Chemists"
        people={chemists}
      />
      <ListSection
        title="Everyone Else"
        people={everyoneElse}
      />
    </article>
  );
}
export const people = [{
  id: 0,
  name: 'Creola Katherine Johnson',
  profession: 'mathematician',
  accomplishment: 'spaceflight calculations',
  imageId: 'MK3eW3A'
}, {
  id: 1,
  name: 'Mario José Molina-Pasquel Henríquez',
  profession: 'chemist',
  accomplishment: 'discovery of Arctic ozone hole',
  imageId: 'mynHUSa'
}, {
  id: 2,
  name: 'Mohammad Abdus Salam',
  profession: 'physicist',
  accomplishment: 'electromagnetism theory',
  imageId: 'bE7W1ji'
}, {
  id: 3,
  name: 'Percy Lavon Julian',
  profession: 'chemist',
  accomplishment: 'pioneering cortisone drugs, steroids and birth control pills',
  imageId: 'IOjWm71'
}, {
  id: 4,
  name: 'Subrahmanyan Chandrasekhar',
  profession: 'astrophysicist',
  accomplishment: 'white dwarf star mass calculations',
  imageId: 'lrWQx8l'
}];
export function getImageUrl(person) {
  return (
    'https://i.imgur.com/' +
    person.imageId +
    's.jpg'
  );
}
ul { list-style-type: none; padding: 0px 10px; }
li {
  margin-bottom: 10px;
  display: grid;
  grid-template-columns: auto 1fr;
  gap: 20px;
  align-items: center;
}
img { width: 100px; height: 100px; border-radius: 50%; }

Вкладені списки в одному компоненті

Створити список рецептів з цього масиву! Для кожного рецепта у масиві виведіть його назву у вигляді <h2> та список інгредієнтів у <ul>.

Для цього потрібно вкласти два різних виклики map.

import { recipes } from './data.js';

export default function RecipeList() {
  return (
    <div>
      <h1>Recipes</h1>
    </div>
  );
}
export const recipes = [{
  id: 'greek-salad',
  name: 'Greek Salad',
  ingredients: ['tomatoes', 'cucumber', 'onion', 'olives', 'feta']
}, {
  id: 'hawaiian-pizza',
  name: 'Hawaiian Pizza',
  ingredients: ['pizza crust', 'pizza sauce', 'mozzarella', 'ham', 'pineapple']
}, {
  id: 'hummus',
  name: 'Hummus',
  ingredients: ['chickpeas', 'olive oil', 'garlic cloves', 'lemon', 'tahini']
}];

Ви можете зробити це одним із способів:

import { recipes } from './data.js';

export default function RecipeList() {
  return (
    <div>
      <h1>Recipes</h1>
      {recipes.map(recipe =>
        <div key={recipe.id}>
          <h2>{recipe.name}</h2>
          <ul>
            {recipe.ingredients.map(ingredient =>
              <li key={ingredient}>
                {ingredient}
              </li>
            )}
          </ul>
        </div>
      )}
    </div>
  );
}
export const recipes = [{
  id: 'greek-salad',
  name: 'Greek Salad',
  ingredients: ['tomatoes', 'cucumber', 'onion', 'olives', 'feta']
}, {
  id: 'hawaiian-pizza',
  name: 'Hawaiian Pizza',
  ingredients: ['pizza crust', 'pizza sauce', 'mozzarella', 'ham', 'pineapple']
}, {
  id: 'hummus',
  name: 'Hummus',
  ingredients: ['chickpeas', 'olive oil', 'garlic cloves', 'lemon', 'tahini']
}];

Кожен з рецептів вже містить поле id, тому зовнішній цикл використовує його для свого key. Не існує ідентифікатора, який можна було б використовувати для циклічного перебору інгредієнтів. Однак, розумно припустити, що той самий інгредієнт не буде вказано двічі у тому самому рецепті, тому його назву можна використати як ключ. Крім того, ви можете змінити структуру даних, додавши ідентифікатори, або використати індекс як ключ (із застереженням, що ви не можете безпечно змінювати порядок інгредієнтів).

Витяг компонента елемента списку

Цей RecipeList компонент містить два вкладених виклики map. Для спрощення витягніть з нього компонент Recipe, який прийматиме пропси id, name та ingredients. Де ви розміщуєте зовнішній ключ і чому?

import { recipes } from './data.js';

export default function RecipeList() {
  return (
    <div>
      <h1>Recipes</h1>
      {recipes.map(recipe =>
        <div key={recipe.id}>
          <h2>{recipe.name}</h2>
          <ul>
            {recipe.ingredients.map(ingredient =>
              <li key={ingredient}>
                {ingredient}
              </li>
            )}
          </ul>
        </div>
      )}
    </div>
  );
}
export const recipes = [{
  id: 'greek-salad',
  name: 'Greek Salad',
  ingredients: ['tomatoes', 'cucumber', 'onion', 'olives', 'feta']
}, {
  id: 'hawaiian-pizza',
  name: 'Hawaiian Pizza',
  ingredients: ['pizza crust', 'pizza sauce', 'mozzarella', 'ham', 'pineapple']
}, {
  id: 'hummus',
  name: 'Hummus',
  ingredients: ['chickpeas', 'olive oil', 'garlic cloves', 'lemon', 'tahini']
}];

Ви можете скопіювати JSX із зовнішньої карти у новий компонент Recipe і повернути цей JSX. Потім ви можете змінити recipe.name на name, recipe.id на id і так далі, і передати їх як пропси до Recipe:

import { recipes } from './data.js';

function Recipe({ id, name, ingredients }) {
  return (
    <div>
      <h2>{name}</h2>
      <ul>
        {ingredients.map(ingredient =>
          <li key={ingredient}>
            {ingredient}
          </li>
        )}
      </ul>
    </div>
  );
}

export default function RecipeList() {
  return (
    <div>
      <h1>Recipes</h1>
      {recipes.map(recipe =>
        <Recipe {...recipe} key={recipe.id} />
      )}
    </div>
  );
}
export const recipes = [{
  id: 'greek-salad',
  name: 'Greek Salad',
  ingredients: ['tomatoes', 'cucumber', 'onion', 'olives', 'feta']
}, {
  id: 'hawaiian-pizza',
  name: 'Hawaiian Pizza',
  ingredients: ['pizza crust', 'pizza sauce', 'mozzarella', 'ham', 'pineapple']
}, {
  id: 'hummus',
  name: 'Hummus',
  ingredients: ['chickpeas', 'olive oil', 'garlic cloves', 'lemon', 'tahini']
}];

Тут <Recipe {...recipe} key={recipe.id} /> є синтаксичним скороченням, яке означає "передати всі властивості об'єкта recipe як пропси компоненту Recipe". Ви також можете написати кожен проп явно: <Recipe id={recipe.id} name={recipe.name} ingredients={recipe.ingredients} key={recipe.id} />.

Зверніть увагу, що ключ вказано на самому <Recipe>, а не на кореневому <div>, повернутому з Recipe. Це пов'язано з тим, що цей ключ потрібен безпосередньо в контексті оточуючого масиву. Раніше у вас був масив <div>s, тому для кожного з них потрібен key, але тепер у вас є масив <Recipe>s. Іншими словами, коли ви витягуєте компонент, не забудьте залишити key за межами JSX, який ви копіюєте і вставляєте.

Список з роздільником

Цей приклад відображає відоме хайку Тачібани Хокуші, кожен рядок якого обгорнуто в тег <p>. Ваше завдання полягає у тому, щоб вставити розділювач <hr /> між кожним абзацом. Отримана структура повинна виглядати наступним чином:

<article>
  <p>I write, erase, rewrite</p>
  <hr />
  <p>Erase again, and then</p>
  <hr />
  <p>A poppy blooms.</p>
</article>

Хайку містить лише три рядки, але ваш розв'язок має працювати з будь-якою кількістю рядків. Зверніть увагу, що елементи <hr /> з'являються лише між елементами <p>, а не на початку чи в кінці!

const poem = {
  lines: [
    'I write, erase, rewrite',
    'Erase again, and then',
    'A poppy blooms.'
  ]
};

export default function Poem() {
  return (
    <article>
      {poem.lines.map((line, index) =>
        <p key={index}>
          {line}
        </p>
      )}
    </article>
  );
}
body {
  text-align: center;
}
p {
  font-family: Georgia, serif;
  font-size: 20px;
  font-style: italic;
}
hr {
  margin: 0 120px 0 120px;
  border: 1px dashed #45c3d8;
}

(Це рідкісний випадок, коли індекс як ключ є прийнятним, оскільки рядки вірша ніколи не змінять порядок).

Вам доведеться або перетворити map на ручний цикл, або використати фрагмент.

Ви можете написати ручний цикл, вставляючи <hr /> та <p>...</p> у вихідний масив на ходу:

const poem = {
  lines: [
    'I write, erase, rewrite',
    'Erase again, and then',
    'A poppy blooms.'
  ]
};

export default function Poem() {
  let output = [];

  // Fill the output array
  poem.lines.forEach((line, i) => {
    output.push(
      <hr key={i + '-separator'} />
    );
    output.push(
      <p key={i + '-text'}>
        {line}
      </p>
    );
  });
  // Remove the first <hr />
  output.shift();

  return (
    <article>
      {output}
    </article>
  );
}
body {
  text-align: center;
}
p {
  font-family: Georgia, serif;
  font-size: 20px;
  font-style: italic;
}
hr {
  margin: 0 120px 0 120px;
  border: 1px dashed #45c3d8;
}

Використання початкового індексу рядка як key більше не працює, оскільки кожен роздільник та абзац тепер знаходяться в одному масиві. Однак ви можете надати кожному з них окремий ключ за допомогою суфікса, наприклад, key={i + '-text'}.

Як варіант, ви можете відрендерити колекцію фрагментів, які містять <hr /> та <p>...</p>. Однак синтаксис стенографії <>...</> не підтримує передавання ключів, тому вам доведеться написати <Fragment> явно:

import { Fragment } from 'react';

const poem = {
  lines: [
    'I write, erase, rewrite',
    'Erase again, and then',
    'A poppy blooms.'
  ]
};

export default function Poem() {
  return (
    <article>
      {poem.lines.map((line, i) =>
        <Fragment key={i}>
          {i > 0 && <hr />}
          <p>{line}</p>
        </Fragment>
      )}
    </article>
  );
}
body {
  text-align: center;
}
p {
  font-family: Georgia, serif;
  font-size: 20px;
  font-style: italic;
}
hr {
  margin: 0 120px 0 120px;
  border: 1px dashed #45c3d8;
}

Пам'ятайте, що фрагменти (часто записуються як <> </>) дозволяють групувати вузли JSX без додавання додаткових <div>s!