React

Zustand: клиентское состояние

Чтобы расшарить одно булево значение isSidebarOpen между шапкой и контентом, разработчик заводит Context: создаёт контекст, оборачивает приложение в Provider, держит useState в провайдере и прокидывает value. Работает - но при каждом изменении ре-рендерится всё поддерево под провайдером, даже компоненты, которым сайдбар безразличен. Альтернатива, Redux, добавляет actions, reducers и кучу обвязки. Zustand задаёт вопрос: а что, если стор это просто хук, без Provider, и компонент подписывается ровно на тот кусок состояния, который ему нужен.

  • UI-состояние приложений: открытые панели, активные модалки, выбранная тема, состояние онбординга
  • Сложные клиентские формы и мастера с шагами, где состояние шарится между несвязанными компонентами
  • Редакторы и канвасы (доски, диаграммы), где выделение, инструмент и зум живут в одном лёгком сторе
  • Команды, заменившие Redux на Zustand ради меньшего объёма кода при той же предсказуемости
  • Связки Zustand плюс TanStack Query: клиентское состояние в Zustand, серверное в Query

Предварительные знания

  • useState и подъём состояния наверх для общего доступа
  • useContext и его поведение при ре-рендерах
  • Идея о том, почему лишние ре-рендеры это проблема производительности
  • 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 занимает середину: подписка на куски как у продвинутых сторов, но почти без церемоний.

СвойствоContextReduxZustand
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. Вместе они покрывают почти весь стейт
Zustand: клиентское состояние

0

1

Войти