// фронтенд
Redux или Zustand: что я реально беру для стейта в проде — и почему чаще всего ни то, ни другое

На собесах за последние пару лет про замыкания и event loop молчат. Спрашивают другое: «Redux или Zustand?» И почти всегда за этим вопросом прячется ожидание, что я назову один правильным, а второй — устаревшим. Я так не отвечаю. И сейчас объясню почему.
За четыре года в финтехе я прошёл полный круг: голый Redux с тоннами boilerplate, переезд на Redux Toolkit, Zustand на новых фичах. А свои два сайта — ishabanov.ru и hire.ishabanov.ru — в итоге собрал вообще почти без глобального стейта.
Ленью тут и не пахнет — это осознанное решение. Главная мысль одним ударом: правильный инструмент — тот, что соответствует размеру задачи, а самый частый правильный ответ в том, что лишний стор тебе просто не нужен.
Откуда боль: Redux и путь к RTK
Мой первый большой проект был на голом Redux с redux-thunk. Чтобы добавить одно поле в стейт, я открывал четыре файла: action types, action creators, reducer и где-нибудь connect с mapStateToProps. На простой CRUD уходило больше кода в инфраструктуре, чем в самой фиче.
Сама идея прекрасна: один store, чистые редьюсеры, предсказуемый поток. Проблема была в церемониях вокруг неё.
Redux Toolkit честно решил большую часть этой боли. createSlice генерирует actions из редьюсеров, Immer даёт писать «мутабельно» поверх иммутабельного стейта, createAsyncThunk и RTK Query закрывают загрузку данных. Boilerplate схлопнулся в разы. Но модель осталась той же: один глобальный store, провайдер на корне, мышление в терминах actions и reducers. Для виджета на два-три флага это всё ещё из пушки по воробьям. RTK сделал Redux приятным, но не лёгким.
Когда мне хватает Zustand
Zustand я беру, когда нужен общий стейт, но не нужна вся машинерия Redux. По сути это просто хук: создаёшь стор функцией create, читаешь его через селектор, и компонент перерисовывается только когда изменился именно его срез. Никакого провайдера на корне, никаких action types.
import { create } from 'zustand';
const useCart = create((set) => ({
items: [],
add: (p) => set((s) => ({ items: [...s.items, p] })),
clear: () => set({ items: [] }),
}));
// в компоненте — подписка ровно на свой срез
const count = useCart((s) => s.items.length);Тут весь стор: состояние и экшены в одном объекте, никаких отдельных файлов. count перерисует компонент только при смене длины корзины — добавишь поле, на которое он не подписан, и его это не тронет. Вот за эту прямоту я Zustand и держу. Мои практические критерии «достаточно Zustand»:
- Состояние локальное или средне-глобальное: корзина, состояние модалок, фильтры, шаги визарда — то, что живёт в пределах фичи и не пронизывает всё приложение.
- Логики ререндеров хватает селекторов — не нужен граф зависимостей между десятками срезов.
- Команда небольшая: дисциплину держим договорённостями, без структуры, которую навязывает фреймворк.
- Не нужен time-travel и пошаговый разбор каждого action в девтулзах как обязательная часть процесса.
Когда я осознанно беру Redux/RTK
А теперь честно про обратную сторону. Именно гибкость Zustand — его слабое место на больших проектах. Стор можно собрать десятком разных способов, и в команде из пятнадцати человек эти способы разъедутся. Redux навязывает один скучный паттерн всем — и это его фича, а не баг. Я выбираю RTK, когда команда большая и важна единая, узнаваемая структура. Когда нужны зрелые девтулзы с тайм-трэвелом — в финтехе при разборе инцидента бесценно отмотать стейт по шагам. Когда побочек много и они сложные, а RTK Query с инвалидацией кэша из коробки экономит недели. На таком масштабе многословность Redux окупается предсказуемостью.
Boilerplate, который раздражает в маленьком проекте, — это и есть страховка, за которую платишь в большом.
Главный принцип: минимум глобального стейта
Но самый недооценённый ответ на «Redux или Zustand» — встречный вопрос: а нужен ли тебе вообще глобальный стор? Очень часто разработчик тянет менеджер стейта туда, где хватило бы пропсов и useState. Глобальный стейт — это связанность. Чем его больше, тем труднее рассуждать о том, кто и когда что меняет.
Свои сайты я собрал именно из этого принципа. Единственный по-настоящему глобальный кусок — тема: один React Context, поднятый на корне, value завёрнут в useMemo. Всё остальное — локальное состояние фич в их собственных хуках: фильтр блога живёт в компоненте блога, состояние формы — в форме. Ни Redux, ни Zustand — и мне ни разу не понадобилось их добавлять.
Так что на собесе я по-прежнему не называю победителя. Контекст — для редко меняющегося значения вроде темы. Локальный стейт — для всего остального. Zustand — когда срез реально пересекает границы фич. RTK — когда масштаб делает его дисциплину выгодной. А чаще всего лишний стор просто не нужен.
P.S. Дальше копнём контент-контракт на Zod как единый источник истины — как одна схема разом проверяет данные на билде, типизирует фронт и не пускает кривой пост на прод.
Есть похожая задача?
Расскажите — вернусь с оценкой в тот же день.


