События и состояние
Шапка и область каталога уже разделены. Добавим первое пользовательское действие: кнопки html, css и javascript будут выбирать одну карточку. Результат — интерфейс, который меняется через состояние компонента, а не через прямое изменение текста найденного DOM-элемента. Этот маленький фильтр пригодится позже, когда на экране окажется весь список.
Работаем с lesson-04/after. Home хранит выбранную тему, находит запись с совпадающим полем topic и выводит её. Начальные данные остаются прежними: двенадцать минут для HTML, двадцать для CSS и двадцать пять для JavaScript. Мы меняем механизм выбора, сохраняя предметный пример.
Ссылка на функцию и её немедленный вызов
Для реакции на кнопку используется onClick. Значением свойства должна стать функция, которую React сможет вызвать при событии. Если написать onClick={setTopic('css')}, вызов произойдёт уже при вычислении JSX. Это не ожидание нажатия. Он может запустить обновление во время рендера и привести к неправильной последовательности действий.
Когда требуется передать конкретную тему, используем небольшую функцию-обёртку. Она получает событие только при действии пользователя и внутри вызывает setter с нужным значением. Сама функция пока лишь передана в JSX. Правильный и неправильный варианты сопоставлены в документации событий React.
| Запись | Когда выполняется действие | Применение |
|---|---|---|
| onClick={handleClick} | При событии | Готовый обработчик |
| onClick={() => setTopic('css')} | При событии | Вызов с аргументом |
| onClick={handleClick()} | Во время вычисления JSX | Обычно ошибка для нашего обработчика |
Кнопка имеет type button. Сегодня она находится вне формы, но это явное назначение помогает при последующей перестройке интерфейса: действие выбора темы не должно случайно стать отправкой формы. Ссылка и кнопка также решают разные задачи. Для перехода по адресу нужна ссылка, для изменения выбранного фильтра — кнопка.
Почему обычной переменной недостаточно
Можно объявить let topic = 'html' внутри Home и присвоить ей css в обработчике. Такое присваивание изменит JavaScript-переменную данного вызова, но не сообщит React о необходимости нового отображения. Кроме того, при следующем вызове компонента локальная инициализация снова начнётся с html. Это не надёжное хранилище текущего выбора интерфейса.
useState связывает значение с экземпляром компонента. Хук возвращает текущий снимок и функцию обновления. Когда setter получает новое значение, React может подготовить следующий результат компонента. Мы читаем topic в конкретном рендере, а не изменяем уже возвращённый JSX. Такая модель объясняет, почему немедленное чтение старой переменной после setter не обязательно покажет будущее значение.
Хуки вызываются на верхнем уровне компонента, в устойчивом порядке. Нельзя вынести useState внутрь условия «если выбрана CSS» или в обработчик кнопки. Состояние должно существовать до решения, какие элементы показать. Иначе связь между вызовами и экземпляром компонента становится неправильной. В нашей маленькой форме достаточно одного состояния темы; число минут вычисляется из выбранной записи.
Полный Home с выбором темы
Заменяем целиком src/pages/Home.jsx:
import {useState} from 'react';
import {initialNotes} from '../data/notes.js';
export default function Home() {
const [topic, setTopic] = useState('html');
const note = initialNotes.find(item => item.topic === topic);
return <section id="catalog"><h1>Учебный каталог</h1>
<div className="filter">{['html', 'css', 'javascript'].map(value =>
<button key={value} type="button" aria-pressed={topic === value} onClick={() => setTopic(value)}>{value}</button>)}</div>
<article className="card"><h2>{note.title}</h2><p>{note.minutes} минут</p><p>{note.body}</p></article>
</section>;
}
В массиве тем каждая кнопка получает известное значение. Обёртка в onClick запоминает именно value соответствующей итерации. При выборе css вычисление find вернёт объект css-layout, и содержание article изменится. Начальное html показывает первую карточку. В этом состоянии мы гарантируем по данным одну запись для каждой из трёх перечисленных тем; обработка пустого результата понадобится для общего списка позже.
Свойство aria-pressed описывает выбранное состояние кнопки. CSS читает тот же признак, поэтому цвет выбранного элемента и смысл для вспомогательных технологий происходят из одной проверки. Если вручную добавить отдельный класс active без связи с topic, легко оставить два выбранных элемента. В нашем варианте сравнение topic с value определяет оба результата.
Имена setTopic и topic — договор внутри программы, а не специальная пара слов, обязательная для React. Мы выбираем их так, чтобы было видно назначение. Начальное значение передаётся в useState. В дальнейшем компонент может существовать на другом экране, но собственное состояние не становится автоматически общим для всех копий Home.
От события к следующему снимку
Последовательность действия проста: браузер создаёт событие, React вызывает выбранный обработчик, setter получает css, следующий рендер читает css и находит соответствующую запись. Изменение DOM выполняет React. Разработчику не нужно отдельно менять title, body и число minutes вручную. Все три поля выбираются из одного объекта, поэтому уменьшается риск смешать части разных карточек.
Если нажать уже выбранную тему, значение не меняется. Не следует использовать такой клик как обязательный механизм принудительного обновления удалённых данных. Выбор темы и повтор сетевого чтения имеют разный смысл; когда появится API, для повтора добавим отдельное действие. Иначе видимая кнопка будет совмещать фильтрацию, запрос и непонятный сброс ошибок.
Объект события нужен, если требуется прочитать сведения о конкретном элементе или остановить стандартное действие. У кнопки с вложенным текстом target может означать более глубокий узел, а currentTarget — элемент обработчика. Но для наших трёх значений не требуется извлекать тему из подписи кнопки: она уже находится в замыкании. Это также позволяет позднее перевести видимую подпись, сохранив внутренний код темы.
Когда обновление зависит от прежнего значения
Текущий фильтр заменяется готовой строкой. Для счётчика или добавления элемента в массив новое значение часто зависит от предыдущего. В таких случаях применяют функциональное обновление: setter получает функцию, которая вычисляет результат из актуального прежнего состояния. Мы воспользуемся этим в callback для скрытия карточки, где несколько действий не должны потерять друг друга.
Не нужно создавать второе состояние для самой выбранной карточки. Если хранить одновременно topic и note, один обработчик может изменить первое и забыть второе. Сейчас note однозначно вычисляется из topic и начального массива. Состояние предназначено для самостоятельной изменяемой информации, а не для каждого промежуточного выражения, которое встречается в компоненте.
Полезный неправильный случай — напрямую менять поле объекта initialNotes, чтобы показать другой текст. Тогда исходные данные общего модуля изменятся вне понятного владельца состояния, а React не получит ясного обновления. Учебный фильтр не редактирует материалы: он выбирает, какой существующий материал отображать. Позже отдельно рассмотрим создание и удаление серверной записи.
Ожидаемое поведение фильтра
После вашего запуска последовательность html → css → javascript должна менять заголовок и длительность согласованно. Выбранной остаётся ровно одна кнопка. Возврат к html возвращает исходное содержание, потому что данные не были изменены. Проверьте управление клавиатурой: native button может получить фокус и выполнить действие без мыши.
Эти наблюдения ещё не получены при подготовке серии: приложение не запускалось. Они задают понятный критерий для проверки будущего читателя. В следующем уроке вместо одной карточки выведем массив и посмотрим, как состояние видно в React DevTools и почему изменяемому списку нужны устойчивые ключи.