← к блогу

// фронтенд

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

Обложка статьи: «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 как единый источник истины — как одна схема разом проверяет данные на билде, типизирует фронт и не пускает кривой пост на прод.

Есть похожая задача?

Расскажите — вернусь с оценкой в тот же день.

Обсудить проект

// читать дальше

Кадр из клипа про то, почему ИИ-агенты рисуют серые интерфейсы
0:36
// видео

Почему ИИ-агенты рисуют одинаково серые интерфейсы

Обложка статьи: «Claude Design в проде»
// дизайн6 мин

Как использовать Claude Design в проде

Я разработчик, дизайнер из меня никакой. Но сайт, который ты читаешь, выглядит как надо — дизайн я собрал с Claude, руками не рисовал. Показываю весь путь и артефакты, которые реально пошли в работу.

Обложка статьи: «1 блог, 2 сайта, 1 сервис»
// архитектура8 мин

Два сайта — один блог: как я не уронил SEO, сливая контент в один сервис

Слил блоги двух сайтов в один сервис на Hono и Turso. База оказалась простой частью, вся коварность — в доставке: контент должен приезжать на билде, потому что при фетче на клиенте превью-боты ловят пустую страницу.