React
useState и состояние компонента
Счётчик лайков, открытое или закрытое модальное окно, текст в поле ввода, активная вкладка - всё это данные, которые меняются прямо во время работы приложения. Обычная переменная тут бесполезна: изменить её можно, но React об этом не узнает и ничего не перерисует. Нужен особый вид памяти, который не только хранит значение между перерисовками, но и говорит React 'данные изменились, перерисуй'. Этот механизм - хук useState, и именно с него начинается живой, реагирующий на действия интерфейс.
- Любой интерактивный элемент - тогглы, аккордеоны, табы, формы - под капотом держит своё состояние в useState
- Корзина, фильтры каталога, шаги визарда в реальных приложениях - это состояние, меняемое иммутабельно
- React 19 с компилятором сам мемоизирует ре-рендеры, но базовая модель useState осталась прежней и обязательна к пониманию
- Для сложного глобального состояния берут Zustand или Redux, но они построены на тех же принципах иммутабельности, что и useState
Предварительные знания
- Компоненты и props: компонент - это функция, которая перевызывается при изменениях
- Метод проектирования: что именно считается состоянием, а что производным значением
- JavaScript: spread-оператор для копирования массивов и объектов, стрелочные функции
Объявление состояния и сеттер
Хук useState вызывают в теле компонента и передают ему начальное значение. Он возвращает массив из двух элементов: текущее значение состояния и функцию для его изменения. Их сразу деструктурируют. Имя сеттера по соглашению начинается с set. Главное отличие от обычной переменной: вызов сеттера не просто меняет значение, а планирует повторный вызов компонента, во время которого useState вернёт уже новое значение.
Почему нельзя обойтись обычной let count = 0? Потому что при ре-рендере функция компонента вызывается заново и локальная переменная сбросилась бы в начальное значение. useState же сохраняет значение между вызовами компонента и связывает его с конкретным экземпляром. Именно поэтому хуки нельзя вызывать в условиях или циклах: React определяет, какому состоянию принадлежит вызов, по порядку хуков.
Хуки вызывают только на верхнем уровне компонента, не внутри if, циклов или вложенных функций. React сопоставляет вызовы useState между рендерами по их порядку, и условный вызов сбивает этот порядок, ломая состояние.
Чем useState отличается от обычной переменной let внутри компонента?
Иммутабельность: копировать, а не мутировать
Для примитивов (числа, строки, булевы) всё просто: сеттер получает новое значение. С объектами и массивами есть критичное правило: их нельзя менять на месте. React сравнивает старое и новое состояние по ссылке, и если мутировать существующий объект, ссылка останется прежней, React не заметит изменения и не перерисует. Поэтому состояние обновляют иммутабельно - создают новый объект или массив на основе старого.
Мутация вроде user.age = 31 или todos.push(...) с последующим setTodos(todos) почти всегда баг: ссылка не изменилась, React считает состояние тем же и не перерисовывает. Всегда создают новый объект или массив.
| Операция | Мутация (нельзя) | Иммутабельно (нужно) |
|---|---|---|
| Добавить в массив | arr.push(x) | [...arr, x] |
| Удалить из массива | arr.splice(i, 1) | arr.filter(...) |
| Изменить поле объекта | obj.field = v | { ...obj, field: v } |
Для глубоко вложенных структур ручной spread становится громоздким. В таких случаях помогает библиотека Immer или встроенный structuredClone, но базовый принцип тот же: на выходе должна быть новая ссылка.
Почему todos.push(item); setTodos(todos) не вызовет перерисовку?
Снимок рендера, функциональные обновления и батчинг
Внутри одного рендера переменная состояния - это снимок: фиксированное значение, которое не меняется по ходу выполнения обработчика. Вызов сеттера не переписывает переменную немедленно, а планирует новый рендер с новым значением. Поэтому несколько setCount(count + 1) подряд, опирающихся на одно и то же значение count из снимка, дадут прибавку всего на единицу, а не на три.
Когда новое значение зависит от предыдущего, используют функциональную форму: сеттер принимает функцию, которой React передаёт самое свежее значение. Так вызовы складываются последовательно, а не опираются на устаревший снимок.
Батчинг: React группирует все вызовы сеттеров внутри одного обработчика и делает один ре-рендер в конце, а не по разу на каждый вызов. С React 18 это работает и в асинхронном коде - в промисах, таймерах, обработчиках событий. Меньше лишних перерисовок без усилий со стороны разработчика.
Внутри одного обработчика setCount(count + 1) вызван три раза подряд. На сколько вырастет count?
Связь с другими темами
useState - сердце интерактивности. На нём держится почти весь второй модуль:
- События и обработчики — Сеттер состояния вызывают из обработчиков событий - onClick, onChange и других
- Подъём состояния — Когда состояние нужно нескольким компонентам, useState переезжает к общему родителю
- Что такое render — Вызов сеттера планирует ре-рендер - это прямой повод понять фазы рендера
Итог
- useState возвращает пару: текущее значение и функцию-сеттер. Вызов сеттера планирует ре-рендер компонента
- Состояние обновляют иммутабельно: создают новый объект или массив, а не мутируют существующий
- Функциональная форма setX(prev => prev + 1) нужна, когда новое значение зависит от предыдущего
- React батчит несколько вызовов сеттеров в одном обработчике в один ре-рендер ради производительности
- Состояние - это снимок конкретного рендера: переменная состояния не меняется по ходу обработчика, новое значение появится только в следующем рендере
Связанные уроки
- rc-05-thinking-in-react — Метод проектирования находит минимальное состояние, а useState - инструмент, которым это состояние объявляют
- rc-09-lifting-state — Подъём состояния перемещает useState в общего родителя, поэтому сначала нужно освоить сам useState
- rc-10-render-mental-model — Понимание состояния как снимка рендера прямо ведёт к модели того, когда и как React перерисовывает