Выбор стейт-менеджера без религии: useState, Zustand, Redux
«Какой стейт-менеджер выбрать?» — вечный вопрос, на который обычно отвечают любимым инструментом. Но выбирать стоит не по моде и не по холиварам в комментариях, а по типу состояния, с которым имеете дело. Большая часть боли со стейтом — от того, что пытаются засунуть всё в один инструмент. Разберём три категории и подход к каждой.
Категория 1. Локальный UI-стейт
Состояние одного компонента, которое больше никому не нужно: открыто ли меню, значение инпута, активная вкладка.
function SearchBox() {
const [query, setQuery] = useState('');
return <input value={query} onChange={e => setQuery(e.target.value)} />;
}
Точка. useState (или useReducer для сложной логики переходов) — единственный правильный ответ. Не нужно тянуть Redux, MobX или глобальный стор ради одного поля. Если состояние локально — держите его локально. Когда окажется, что оно нужно ещё кому-то — поднимите наверх или вынесите в общий стор. Не преждевременно обобщайте.
Категория 2. Серверный стейт — отдельный зверь
Самая частая ошибка — хранить данные с API в Redux/Zustand/Context вручную: написать экшен fetchUsers, в нём fetch, потом dispatch(setUsers(data)), плюс флаги loading/error, плюс инвалидация кеша. Это путь к боли: дублирование, гонки запросов, устаревший кеш, ручная дедупликация.
Серверный стейт качественно отличается от клиентского: у него есть источник истины на сервере, он асинхронный, его надо рефетчить и инвалидировать. Для него существуют специализированные библиотеки:
- TanStack Query (React Query) — де-факто стандарт. Кеш по ключу запроса, автоматический рефетч (на фокус окна, по интервалу, после мутации), дедупликация одновременных запросов, optimistic updates, бесшовная пагинация.
- SWR — проще и легче, от Vercel; основной набор возможностей похож.
- RTK Query — если уже на Redux Toolkit, встроен в него.
function UserList() {
const { data, isLoading, error } = useQuery({
queryKey: ['users'],
queryFn: () => fetch('/api/users').then(r => r.json())
});
if (isLoading) return <Spinner />;
if (error) return <ErrorView />;
return data.map(u => <UserRow key={u.id} user={u} />);
}
Никаких экшенов, никаких флагов, никакого бойлерплейта. Кеш, рефетч, инвалидация — на стороне библиотеки. Серверные данные не должны лежать в клиентском сторе вручную.
Категория 3. Глобальный клиентский стейт
Состояние, которое нужно во многих местах приложения и не связано с сервером: тема оформления, авторизация пользователя (клиентская часть), настройки UI, выбранный язык.
Здесь 90% случаев покрывают Zustand или Jotai — современные минималистичные сторы без экшенов, редьюсеров и провайдеров:
import { create } from 'zustand';
const useThemeStore = create((set) => ({
theme: 'light',
toggleTheme: () => set((s) => ({ theme: s.theme === 'light' ? 'dark' : 'light' })),
}));
// В любом компоненте, без пропсов и провайдеров
function ThemeToggle() {
const theme = useThemeStore((s) => s.theme);
const toggle = useThemeStore((s) => s.toggleTheme);
return <button onClick={toggle}>{theme}</button>;
}
Подписка на конкретный срез (useThemeStore(s => s.theme)) означает, что компонент перерендеривается только при изменении этого поля. Никакого Context.Provider, который перерендеривает всех потребителей при любом изменении.
Где Redux всё ещё оправдан
Redux (с Redux Toolkit) — не зло, просто инструмент со своей нишей. Он по-прежнему имеет смысл, когда:
- Большие команды с жёсткими соглашениями. Единственный путь изменения состояния через экшены и редьюсеры — это самодокументируемый, предсказуемый поток. Легче онбординг, проще ревью.
- Сложная логика обновления. Когда изменения состояния нетривиальны (асинхронные последовательности, саги, реакции на экшены разных модулей) — middleware Redux (Redux Saga, RTK Listener) решают это чище, чем ручной код.
- Нужны devtools с time-travel. Запись всех экшенов, перемотка состояния назад — мощный инструмент отладки для сложных доменов.
Для большинства новых проектов малого и среднего размера Redux избыточен. Но он не «устарел» — он просто перестал быть дефолтом.
Антипаттерны
Дублирование серверных данных в клиентском сторе. Самая частая боль. Список пользователей хранится и в React Query (правильно), и в Zustand «для удобства» (неправильно). Два источника истины → рассинхрон → баги. Серверные данные — только в кеше React Query/SWR.
Прокидывание всего через Context. Context.Provider при изменении значения перерендеривает всех потребителей, даже тех, кому нужна другая часть контекста. Для темы это нормально (она меняется редко), для поиска или фильтров — катастрофа. Если контекст меняется часто — разбейте его или используйте Zustand с точечной подпиской.
Один гигантский стор на приложение. Когда в одном Zustand-сторе живут данные пользователя, тема, корзина, фильтры и состояние форм — это а-ля Redux-2015, только без middleware. Разделяйте по доменам: отдельный стор для темы, отдельный для UI, серверные данные в Query.
Превентивное обобщение. Создать стор «на будущее», когда состояние ещё локальное. Не делайте так. Поднимайте и обобщайте, когда реальная потребность возникнет — рефакторинг из useState в Zustand занимает пять минут.
Фреймворк выбора
Задайте три вопроса про конкретные данные:
- Что это за данные? — UI-стейт, серверные или глобальные клиентские.
- Кто их источник истины? — сервер → React Query; текущий компонент →
useState; несколько компонентов → Zustand. - Как часто они меняются? — редко → Context годится; часто → Zustand с точечной подпиской.
Короткое summary
Стейт-менеджмент — не один инструмент, а три разные категории. Локальный UI-стейт — useState/useReducer, точка. Серверный стейт (данные API, кеш, инвалидация) — React Query / SWR / RTK Query, не кладите их в стор вручную. Глобальный клиентский стейт (тема, авторизация, настройки) — Zustand или Jotai в большинстве случаев; Redux имеет смысл для больших команд со сложной логикой обновления и потребностью в devtools. Главные антипаттерны: дублирование серверных данных в клиентском сторе, Context для часто меняющихся данных, один стор на всё приложение.
Что почитать
- TanStack Query: Overview — концепции серверного стейта и кеша.
- Zustand: Getting Started — минималистичный стор.
- Kent C. Dodds: «Application State Management with React» — практический разбор React Query от создателя популярных статей.