Перейти к содержанию

Списки и React DevTools

В предыдущем уроке выбранная тема определяла одну карточку. Для каталога этого мало: пользователю нужны все подходящие записи, а разработчику — способ понять, какое состояние сейчас использует компонент. Соединим эти задачи. Добавим режим «all», выведем массив через map и разберём, чем React DevTools помогает при поиске ошибки в списке.

Работаем с теми же тремя учебными записями: «Семантика HTML», «Сетка карточек CSS» и «Состояние в JavaScript». Их идентификаторы уже заданы в данных. Никаких запросов пока нет: переключение темы меняет только отображение массива в памяти страницы. Это позволяет отделить проблему разметки от проблем сети и сервера.

Из одной карточки получается список

Метод filter отвечает на вопрос, какие записи подходят выбранной теме. Метод map решает другую задачу: во что превратить каждую подходящую запись. Мы сначала получаем visible, затем создаём элементы li. Порядок операций существенен: если фильтровать уже созданную разметку, придётся извлекать сведения из элементов вместо обычных объектов данных.

Режим all означает отсутствие ограничения по теме. Это договорённость нашего приложения, а не особое значение React. Когда topic равен html, условие пропускает только первую запись. Когда topic равен all, левая часть условия становится истинной для каждой записи. Исходный initialNotes при этом не меняется, поэтому переключение обратно восстанавливает весь список.

Вот полный файл src/pages/Home.jsx для этого этапа:

import {useState} from 'react';
import {initialNotes} from '../data/notes.js';
export default function Home() {
  const [topic, setTopic] = useState('all');
  const visible = initialNotes.filter(note => topic === 'all' || note.topic === topic);
  return <section id="catalog"><h1>Учебный каталог</h1>
    <div className="filter">{['all', 'html', 'css', 'javascript'].map(value =>
      <button key={value} type="button" aria-pressed={topic === value} onClick={() => setTopic(value)}>{value}</button>)}</div>
    <ul className="cards">{visible.map(note => <li key={note.id} className="card"><article>
      <h2>{note.title}</h2><p>{note.topic} · {note.minutes} минут</p><p>{note.body}</p>
    </article></li>)}</ul>
  </section>;
}

Обратите внимание на фигурные скобки вокруг вызова map. Внутри JSX мы вычисляем JavaScript-выражение, результатом которого становится массив React-элементов. Дополнительный join не требуется. Напротив, попытка склеить карточки в строку уничтожит их структуру и превратит управляемые элементы интерфейса в текст.

Внешний ul описывает список, а li — отдельную позицию. Заголовок карточки находится внутри article. Такая структура сохраняет смысл при чтении страницы без оформления. Сам массив не определяет правильный HTML: разработчик по-прежнему выбирает элементы и следит, чтобы вложенность отвечала назначению содержимого.

Почему ключом служит идентификатор

При следующем рендере React сопоставляет элементы списка с предыдущими. Для этого мы передаём key={note.id}. Ключ должен оставаться связанным с одной записью и быть уникальным среди соседних элементов данного массива. Название урока плохо подходит: редактор может переименовать материал, а одинаковые названия вполне допустимы в разных разделах.

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

Случайное число также не решает задачу. Если генерировать key при каждом рендере, элемент каждый раз будет выглядеть новым. Это мешает сохранению состояния и заставляет React заново создавать соответствующую ветку. Идентификатор создают при появлении записи, затем хранят вместе с ней; его не придумывают в момент отображения. Эти требования описаны в документации по спискам.

Выбор ключа Что произойдёт при смене порядка Решение для нашего каталога
note.id Ключ следует за записью Используем
index Ключ следует за позицией Не используем
note.title Переименование меняет ключ Оставляем заголовком
Случайное значение Каждая карточка выглядит новой Не генерируем при рендере

Ключ не появляется как обычный атрибут DOM и не передаётся дочернему компоненту в виде props.key. Если отдельному компоненту понадобится идентификатор, его следует передать отдельным пропсом. Сейчас li содержит карточку непосредственно, поэтому дополнительной передачи нет.

Что смотреть в React DevTools

Обычная панель Elements показывает результат: ul, li, кнопки и текст. React DevTools показывает дерево компонентов и данные выбранного компонента. Для нашей страницы полезно найти Home и сопоставить значение topic с активной кнопкой и количеством карточек. Это разные представления одной ситуации, а не конкурирующие способы просмотра страницы.

Дерево компонентов и панель состояния React DevTools

React DevTools: дерево компонентов и состояние выбранного компонента. Архивный пример интерфейса; названия компонентов и данные относятся к демонстрации.

Снимок иллюстрирует расположение инструментов. Показанные App, Navbar, Home и значения состояния относятся к архивной демонстрации, а не к выполнению нашего проекта. В нашем компоненте хранится тема каталога, и данные другие. Внешний вид расширения тоже может отличаться: ориентируйтесь на назначение дерева и панели состояния, а не на совпадение каждой иконки.

Если при выборе css остаётся карточка HTML, сначала смотрим topic. Значение html означает, что событие не изменило состояние. Значение css при неверной карточке направляет нас к условию filter или данным note.topic. Так мы разделяем два возможных дефекта и не меняем обработчик, когда ошиблась модель записи.

Редактирование состояния через инструменты разработчика подходит для исследования интерфейса, но не сохраняет изменения в исходнике. После перезагрузки компонент снова получает своё начальное значение. Аналогично наличие вкладки Profiler не доказывает, что приложение быстрое: для вывода о производительности нужен отдельный сценарий измерения. В этом уроке измерения не выполняются.

Как сопоставить ожидаемый результат с данными

При начальном all должны отображаться три карточки в порядке исходного массива. Для html ожидается одна карточка с идентификатором html-start; для css — css-layout; для javascript — js-state. Это ожидание вытекает из учебных данных. Оно не является отчётом о состоявшемся запуске.

Полезная ошибка для разбора — опечатка javascript в значении кнопки. Если написать js, filter вернёт пустой массив, хотя соответствующая запись присутствует. В таком случае важны обе стороны сравнения: значение состояния и поле данных. Добавление четвёртой карточки не исправит несовпадение словаря тем.

Другой неправильный вариант — note.id, случайно одинаковый у двух записей. React может предупредить о повторяющихся ключах, но это не повод подавлять сообщение случайными числами. Исправляют данные или способ получения идентификатора. Позже сервер будет назначать новые идентификаторы, а клиент — проверять их уникальность.

Пока весь вывод находится в Home, и изменения разметки приходится делать рядом с фильтром. Следующим шагом вынесем список в самостоятельный компонент. Он получит уже подготовленные данные и сможет отобразить разные подборки без собственного знания о выбранной теме.

Оглавление серии