Props в React: повторное использование компонента
Каталог уже показывает несколько карточек и умеет ограничивать их по теме. Теперь представим два блока на одной странице: общий список и отдельную подборку JavaScript. Если скопировать всю разметку, изменение подписи или структуры карточки придётся повторять. Вынесем отображение в LessonList и передадим ему данные через props.
Такое разделение полезно не только для сокращения файла. Home будет выбирать записи, а LessonList — показывать переданный массив. Компонент списка не должен самостоятельно читать initialNotes или решать, что считать темой страницы. Его задача уже: получить заголовок и записи, затем построить понятный интерфейс.
Какие данные нужны списку
Начнём с двух пропсов: title и notes. Первый используется в заголовке раздела, второй — в map. Названия выбираем по смыслу, а не по обязательному словарю React. Если вызвать компонент с items вместо notes, функция не получит нужный массив. Поэтому договорённость между вызывающим компонентом и параметрами важнее краткости названия.
Полный файл src/components/LessonList.jsx выглядит следующим образом:
export default function LessonList({title, notes}) {
return <section><h2>{title}</h2>{notes.length === 0 ? <p>В подборке пока нет карточек.</p> :
<ul className="cards">{notes.map(note => <li key={note.id} className="card"><article>
<h3>{note.title}</h3><p>{note.topic} · {note.minutes} минут</p><p>{note.body}</p>
</article></li>)}</ul>}</section>;
}
Деструктуризация в параметрах извлекает title и notes из объекта props. Тот же код можно написать через props.title и props.notes, но оба варианта описывают один интерфейс. Сокращение не создаёт глобальных переменных и не делает доступными данные соседнего компонента. Компоненты обмениваются только тем, что действительно передано.
Список сразу обрабатывает пустой массив. Сообщение «В подборке пока нет карточек» означает успешное получение набора из нуля записей. Оно пока не связано с загрузкой или ошибкой API: сети на этом этапе нет. Такое различение пригодится дальше, когда пустое состояние станет одним из результатов запроса.
Два вызова одного компонента
Заменим Home целиком. Здесь оба массива построены из одного учебного источника, но передаются в разные экземпляры LessonList:
import {initialNotes} from '../data/notes.js';
import LessonList from '../components/LessonList.jsx';
export default function Home() {
return <section id="catalog"><h1>Учебный каталог</h1>
<LessonList title="Все карточки" notes={initialNotes} />
<LessonList title="JavaScript" notes={initialNotes.filter(note => note.topic === 'javascript')} />
</section>;
}
Первый вызов получает все три записи. Второй получает результат filter с одной записью js-state. Это не два режима одного скрытого списка: в дереве компонентов существуют два экземпляра, каждый со своими входными данными. Изменение заголовка одного вызова не должно менять подпись другого.
Одна и та же карточка JavaScript появляется в обоих списках. Это нормально. Требование уникальности ключей относится к соседним элементам конкретного массива, а не ко всем ключам во всём приложении. Внутри каждого ul идентификаторы остаются уникальными. Не нужно добавлять случайный префикс только потому, что запись показана в другом разделе.
| Вызов | Значение title | Переданные записи | Ожидаемый размер |
|---|---|---|---|
| Первый LessonList | Все материалы | initialNotes | Три |
| Второй LessonList | Только JavaScript | Результат filter | Одна |
| Отдельный пустой пример | Новая подборка | Пустой массив | Ноль, с пояснением |
Мы используем h2 для названия подборки и h3 для карточки, потому что h1 страницы находится в Home. Если впоследствии LessonList будет встроен в другую структуру документа, уровень заголовков нужно будет пересмотреть. Переиспользование компонента не освобождает от проверки смысловой иерархии страницы.
Props представляют входной снимок
React рассматривает props как данные только для чтения. LessonList не должен выполнять notes.push или менять notes[0].title. Массив принадлежит источнику, который передал его компоненту. Такая мутация может затронуть другой список, но не сообщит React о новом состоянии. Внешне это выглядит как случайная рассинхронизация.
В нашем случае оба списка используют объекты из initialNotes. filter создаёт новый массив, но не копирует каждый объект глубоко. Поэтому изменение поля карточки внутри второго списка затронуло бы тот же объект, который читает первый. Этот пример хорошо показывает, почему новый массив сам по себе не означает независимость вложенных данных.
Корректный список ничего не меняет. Он вычисляет элементы из текущего входа и возвращает их. Если пользователь захочет убрать карточку, событие должно сообщить об этом владельцу данных. Такой обратный вызов добавим в следующем уроке. О принципе входных снимков можно прочитать в документации по props.
Не стоит копировать notes в локальное состояние только потому, что компонент получил массив. Это создаст второй источник истины: родитель передаст новые данные, а копия останется прежней. Локальное состояние требуется для действительно самостоятельного поведения, например раскрытия карточки. Сам список входных записей на нашем этапе самостоятельным не является.
Где возникают ошибки с областью видимости
Предположим, что внутри LessonList написано initialNotes.map, хотя импорт остался только в Home. Функция не увидит такую переменную автоматически. Файлы и функции имеют свою область видимости. Исправлением будет использование переданного notes, а не добавление нового импорта, который тайно свяжет список с одним источником.
Другая ошибка — передать строку notes="initialNotes". Кавычки задают текст, а не обращение к переменной. Правильный вызов использует фигурные скобки: notes={initialNotes}. В случае строки у компонента может существовать свойство length, но map уже не будет методом массива. Поэтому внешне похожее выражение нарушает контракт.
Можно добавить проверку типов или схему входных данных, однако в этом уроке договорённость поддерживается явной структурой вызовов. Мы не заявляем, что компонент безопасно принимает любые значения. Ожидается массив записей с нужными полями; позже серверные данные будут проверяться перед попаданием в интерфейс.
Что меняется при редактуре карточки
Если убрать вывод body, оба списка изменятся в одном месте. Если требуется скрыть описание только в компактном блоке, лучше добавить понятный пропс с ограниченным назначением, чем проверять текст title внутри компонента. Заголовок предназначен для читателя и может измениться, поэтому использовать его как переключатель поведения ненадёжно.
Наш текущий интерфейс намеренно небольшой. Мы не вводим десяток опций на случай неизвестных будущих задач. Два пропса уже позволяют показать разные подборки и объяснить направление данных. Новое поведение будем добавлять тогда, когда появится конкретное действие пользователя.
После самостоятельного запуска ожидаются общий список из трёх карточек и подборка JavaScript из одной. При изменении body записи js-state оба отображения должны получить одинаковый текст из данных. Пример подготовлен как исходник; выполнение и измерения в рамках написания курса не проводились. Далее добавим кнопку скрытия и разберём, почему изменение остаётся в Home, хотя нажимают кнопку внутри LessonList.