Состояние — самая частая причина переусложнённого frontend. Команда тянет глобальный стор на всё подряд, кладёт туда данные с сервера, и приложение превращается в клубок синхронизаций. Корень в том, что состояние бывает разной природы, а инструмент берут один на всё. Продукт-мышление здесь — выбрать минимальный инструмент под конкретный вид состояния.
Локальное состояние
Большая часть состояния — локальная: открыт ли дропдаун, что введено в поле, какая вкладка активна. Это useState, и держать его нужно как можно ближе к месту использования.
function SearchBox() {
const [query, setQuery] = useState("");
return <input value={query} onChange={(e) => setQuery(e.target.value)} />;
}
Когда переходов состояния несколько и они связаны (форма с шагами, сложный виджет), вместо россыпи useState берут useReducer — он собирает переходы в одном месте, как маленький конечный автомат.
Разделяемое состояние и context
Когда состояние нужно нескольким компонентам, его поднимают к общему родителю. Если прокидывать через много уровней неудобно, берут context.
const ThemeContext = createContext<"light" | "dark">("light");
function useTheme() {
return useContext(ThemeContext);
}
Но у context есть цена: при изменении значения перерисовываются все потребители. Поэтому context хорош для редко меняющегося (тема, текущий пользователь, локаль) и плох для часто меняющегося (позиция курсора, текст поля) — там он вызывает лавину ререндеров. Это не «глобальный стор», а способ передачи; для частых обновлений нужны другие инструменты.
Серверное состояние ≠ клиентское
Ключевое различие, которое снимает половину сложности: данные с сервера — это не состояние приложения, это кеш серверного состояния. Список товаров, профиль, заказы — они живут на сервере; на клиенте у тебя их временная копия, которую надо обновлять, инвалидировать, перезапрашивать.
Складывать серверные данные в useState/useReducer/стор — ошибка: ты вручную пишешь то, что давно решено (кеширование, гонки, повторные загрузки, инвалидация). Для серверного состояния есть специальный инструмент — TanStack Query, и именно туда уходит большая часть того, что новички кладут в глобальный стор.
Когда внешний стор
После того как серверное состояние ушло в Query, а локальное — в useState, для глобального стора (Zustand, Redux) остаётся мало: по-настоящему общее клиентское состояние, не относящееся к серверу (содержимое корзины до отправки, сложное состояние мастера на много экранов, глобальные UI-флаги).
Критерий простой: стор оправдан, когда состояние (1) клиентское (не кеш сервера), (2) разделяется многими несвязанными компонентами и (3) часто меняется (context не подходит из-за ререндеров). Если хоть одно не выполняется — скорее всего, стор преждевременен. Начинать проект с Redux «на всякий случай» — типичное переусложнение.
Где это в UCP
Выбор состояния — это продуктовое и инженерное решение: минимальный инструмент под природу данных, а не один стор на всё. Локальное — useState/useReducer; редко меняющееся общее — context; серверное — Query; по-настоящему глобальное клиентское — стор, и только оно. Эта дисциплина — то же «не платить сложностью авансом», что и уровни зрелости в backend: продукт-инженер берёт ровно столько механики, сколько требует задача.