Состояние и хуки
Что это такое
Состояние — это данные, которые компонент помнит между рендерами. В функциональном компоненте его добавляют хуком: useState возвращает текущее значение и функцию-сеттер, которая его заменяет, а вызов сеттера планирует новый рендер — и новое значение появляется именно в нём.
Почему это важно
Без состояния компонент может показать лишь то, что ему передали, поэтому любое взаимодействие — открытое меню, введённый символ, выбранная строка — пришлось бы держать в родителе и протягивать вниз. Состояние оставляет эту бухгалтерию локальной и позволяет React самому решать, когда перерисовывать. Большинство ошибок React — ошибки состояния: значение, которое никогда не обновляется; две копии одного факта, которые со временем расходятся; сеттер, вызванный из устаревшего замыкания. Разобравшись, что на самом деле делает обновление состояния, вы избавитесь от всех трёх.
Ключевые идеи
Состояние — это снимок, а не живая переменная
Каждый рендер захватывает значения, с которыми был вызван. count внутри обработчика принадлежит тому рендеру, который этот обработчик создал: два вызова setCount(count + 1) в одном событии видят один и тот же count и прибавляют один, а не два.
function Counter() {
const [count, setCount] = useState(0);
function addTwo() {
setCount((c) => c + 1);
setCount((c) => c + 1);
}
return <button onClick={addTwo}>{count}</button>;
}
Форма с функцией-обновителем получает ожидающее значение, а не копию из рендера, поэтому она безопасна по умолчанию всюду, где следующее значение зависит от предыдущего.
Не храните то, что можно вычислить
Если значение выводится из свойств или из другого состояния, вычисляйте его во время рендера, а не храните. Иначе два факта придётся согласовывать вечно, и пользователь увидит именно тот, про который забыли.
// Так лучше
const visible = items.filter((item) => item.done === false);
// Так не надо — `visible` в конце концов разойдётся с `items`
const [visible, setVisible] = useState(items.filter((item) => item.done === false));
useMemo берите только тогда, когда вычисление измеримо дорого, а не для защиты правила, которое вы уже нарушили.
Держите состояние у его низшего общего владельца
Когда два компонента должны согласованно иметь одно значение, поднимите состояние к их ближайшему общему родителю и передайте значение вниз вместе с колбэком для его изменения. Это и называют поднятием состояния. А вот перенос в глобальное хранилище раньше, чем значение вообще понадобилось другим, добавляет косвенность, которую потом не убрать без перепроектирования.
Правила хуков — цена этой конструкции
Хуки вызывают на верхнем уровне компонента или другого хука — никогда внутри условия, цикла или вложенной функции. Внутренним слотам компонента React сопоставляет хуки по порядку вызова, поэтому условный хук сдвигает каждый следующий на единицу, и состояние достаётся не той переменной. За этим следит правило линтера; держите его включённым.
Обновления собираются, затем следует один рендер
Обновления одного события React собирает вместе и рендерит один раз. Значение текущего рендера сеттер не меняет, поэтому чтение состояния сразу после установки возвращает старую копию. Так и задумано: одно событие не порождает три несогласованных кадра.
Практика
Соберите список с фильтром
Создайте список задач с текстовым полем, которое фильтрует их по мере ввода. Держите задачи и запрос в состоянии, а видимый список выводите во время рендера.
Готово, когда: ввод показывает отфильтрованный список, очистка поля возвращает все строки, и нет ни одной переменной состояния, значение которой — отфильтрованная копия другой.
Исправьте двойной подсчёт
Напишите компонент с кнопкой, которая должна прибавлять к счётчику два. Сначала реализуйте её неверно, двумя вызовами setCount(count + 1), посмотрите на результат, затем перейдите к форме с функцией-обновителем.
Готово, когда: кнопка прибавляет ровно два, и вы можете одной фразой объяснить, почему первая версия прибавила один.
Частые ошибки
- Чтение состояния сразу после установки — обработчик видит снимок рендера, а не новое значение; исправить: вычисляйте следующее значение из аргумента функции-обновителя, а не читая переменную.
- Хранение производных значений — копия расходится с источником, и экран врёт; исправить: вычисляйте во время рендера и запоминайте только то, что измеримо медленно.
- Копирование свойств в состояние — последующие изменения свойства игнорируются, потому что состояние хранит своё первое значение; исправить: читайте свойство напрямую или задайте компоненту ключ, чтобы он пересоздавался.
- Условный вызов хука — состояние привязывается не к тому хуку, а ошибки всплывают далеко от причины; исправить: вызывайте каждый хук безусловно, а ветвление делайте внутри него.
- Дублирование общего состояния в двух компонентах — две копии расходятся, и исправление требует третьего обновления в третьем месте; исправить: поднимите состояние к ближайшему общему родителю.
Что читать дальше
- Компоненты и свойства (02-frontend/29-react/02-components-and-props) — правила границ решают, какой компонент вообще должен владеть состоянием.
- Эффекты и загрузка данных (02-frontend/29-react/04-effects-and-data-fetching) — что делать, когда работа не является вычислением и должна происходить после рендера.
- Управление состоянием (02-frontend/23-state-management) — момент, когда локального состояния перестаёт хватать.