React
Zustand: клиентское состояние
Чтобы расшарить одно булево значение isSidebarOpen между шапкой и контентом, разработчик заводит Context: создаёт контекст, оборачивает приложение в Provider, держит useState в провайдере и прокидывает value. Работает - но при каждом изменении ре-рендерится всё поддерево под провайдером, даже компоненты, которым сайдбар безразличен. Альтернатива, Redux, добавляет actions, reducers и кучу обвязки. Zustand задаёт вопрос: а что, если стор это просто хук, без Provider, и компонент подписывается ровно на тот кусок состояния, который ему нужен.
- UI-состояние приложений: открытые панели, активные модалки, выбранная тема, состояние онбординга
- Сложные клиентские формы и мастера с шагами, где состояние шарится между несвязанными компонентами
- Редакторы и канвасы (доски, диаграммы), где выделение, инструмент и зум живут в одном лёгком сторе
- Команды, заменившие Redux на Zustand ради меньшего объёма кода при той же предсказуемости
- Связки Zustand плюс TanStack Query: клиентское состояние в Zustand, серверное в Query
Предварительные знания
- useState и подъём состояния наверх для общего доступа
- useContext и его поведение при ре-рендерах
- Идея о том, почему лишние ре-рендеры это проблема производительности
Откуда взялся минимальный стор
Zustand (по-немецки состояние) появился в 2019 году в коллективе Poimandres, известном по react-three-fiber. Авторам нужен был стор для 3D-сцен, где Redux ощущался тяжёлым, а Context вызывал лишние ре-рендеры. Идея получилась радикально простой: стор это хук, созданный функцией create, без провайдеров и без обязательного редьюсера. Компонент сам выбирает селектором нужный кусок и ре-рендерится только при его изменении. К середине 2020-х Zustand стал дефолтным выбором для клиентского состояния в React, когда Context уже мал, а Redux ещё избыточен.
Стор как хук через create()
В Zustand стор создаётся функцией create. Она принимает функцию инициализации, которой передан set, и возвращает хук. Внутри объявляются и состояние, и действия, меняющие его через set. Никакого провайдера оборачивать не нужно: хук обращается к стору напрямую из любого компонента.
Действия живут прямо в сторе как обычные функции и меняют состояние через set. set делает поверхностное слияние объекта, поэтому достаточно вернуть только изменившиеся поля. Никаких отдельных action-типов, reducer-функций и dispatch - стор это данные плюс методы в одном месте.
Отсутствие Provider это не косметика. Стор это модуль-синглтон, к которому есть доступ и из React через хук, и из обычного кода через getState и subscribe. Это удобно для интеграции с не-React частями: вебсокетами, таймерами, аналитикой.
Что принципиально отличает создание стора в Zustand от настройки Context?
Селекторы против лишних ре-рендеров
Главная сила Zustand в подписке. Хук вызывается с селектором - функцией, которая выбирает из стора нужный кусок. Компонент перерисовывается только тогда, когда именно этот кусок изменился, а не при любом изменении стора. Это прямое решение проблемы Context, где любое обновление value ре-рендерит всё поддерево под провайдером.
Селектор, возвращающий новый объект на каждом вызове, ломает оптимизацию: { a: s.a, b: s.b } создаёт новую ссылку каждый раз, и компонент ре-рендерится без причины. Для выбора нескольких полей берут useShallow, чтобы сравнение шло поверхностно по значениям, а не по ссылке.
Чем уже селектор, тем реже компонент перерисовывается. Выбор одного примитива (строки, числа, булева) сравнивается по значению и не требует ничего дополнительного. Выбор нескольких полей оборачивают в useShallow. Так стор может быть большим и общим, но каждый компонент платит ре-рендером только за свою часть.
Зачем при чтении из стора Zustand передавать узкий селектор?
Zustand против Context и Redux
У трёх инструментов разные сильные стороны. Context это встроенный механизм передачи значения вниз по дереву, без подписки на части: любое изменение value ре-рендерит всех потребителей. Redux даёт строгий предсказуемый поток (action - reducer - store) и мощные devtools, но требует заметного объёма обвязки. Zustand занимает середину: подписка на куски как у продвинутых сторов, но почти без церемоний.
| Свойство | Context | Redux | Zustand |
|---|---|---|---|
| Provider | Обязателен | Обязателен (store) | Не нужен |
| Ре-рендеры | Всё поддерево при смене value | Точечные через селекторы | Точечные через селекторы |
| Boilerplate | Низкий, но без подписок | Высокий (actions/reducers) | Минимальный |
| Доступ вне React | Нет | Есть (store API) | Есть (getState/subscribe) |
| Где уместно | Редко меняющиеся значения (тема, locale) | Крупные приложения со сложным потоком | Большинство клиентского состояния |
Практическое правило 2026 года: Context хорош для редко меняющихся значений вроде темы или локали, где ре-рендер поддерева не страшен. Redux оправдан в крупных приложениях, где ценны строгий поток и развитые инструменты отладки. Для большинства же клиентского состояния Zustand даёт точечные ре-рендеры при минимуме кода.
Отдельная важная мысль: ничего из этого не предназначено для серверных данных. Список с сервера, профиль, цены это серверное состояние, и его держит TanStack Query с его кэшем и свежестью. Типичная связка - Zustand под чисто клиентское состояние (модалки, выбранный инструмент, черновики) и Query под всё, что приходит с сервера.
Если поле в сторе это копия ответа сервера, которую приходится вручную обновлять, это сигнал перенести его в TanStack Query. Zustand не должен превращаться в самописный кэш серверных данных - это как раз та боль, ради которой и появился Query.
Команде нужно общее клиентское состояние (открытые панели, выбранный инструмент редактора), которое часто меняется. Что разумнее по сравнению с чистым Context?
Связь с другими темами
Этот урок про клиентское состояние. Рядом стоят соседние инструменты:
- useContext — Тот же сценарий общего состояния, но Context ре-рендерит всё поддерево, а Zustand - только подписчиков нужного куска
- TanStack Query — Разделение ответственности: серверное состояние держит Query, клиентское - Zustand, и они не дублируют друг друга
Итог
- Zustand создаёт стор функцией create(): стор это хук, без Provider и без обязательного редьюсера
- Компонент подписывается селектором на конкретный кусок состояния и ре-рендерится только при изменении именно этого куска
- В отличие от Context, изменение стора не ре-рендерит всё поддерево, а только тех, кто подписан на изменившееся значение
- В отличие от Redux, нет actions, reducers и обширного boilerplate: действия это обычные функции в самом сторе
- Селектор это ключевой приём против лишних ре-рендеров: чем уже выбор, тем реже компонент перерисовывается
- Связка Zustand плюс TanStack Query даёт чистое разделение: клиентское состояние в Zustand, серверное в Query
Связанные уроки
- rc-14-usecontext — Zustand решает ту же задачу общего состояния, что и Context, но без ре-рендера всего поддерева
- rc-36-tanstack-query — Серверное состояние держит TanStack Query, клиентское - Zustand. Вместе они покрывают почти весь стейт