Когда использовать Zustand в React: практические критерии выбора
Разбираем, когда Zustand действительно уместен в React, где он выигрывает у Context и Redux, и какие ошибки чаще всего появляются в production.
- Введение
- Короткий ответ для практики
- Как понять, что Zustand уже нужен
- Состояние живет дольше одного компонента
- Обновления частые, а Context начинает ререндерить лишнее
- Нужно разделяемое client state, а не данные сервера
- Архитектурный разбор: где Zustand лежит в схеме приложения
- Контекст задачи
- Схема компонентов и ролей
- Поток события
- Узкие места и точки отказа
- Код-пример 1: сценарий, где Zustand действительно уместен
- Код-пример 2: сценарий, где Zustand уже выглядит сомнительно
- Сравнительная таблица: когда использовать Zustand, а когда нет
- Production-ловушки: где чаще всего ломают Zustand
- 1. Store становится свалкой всего подряд
- 2. Компоненты подписываются на слишком широкий кусок
- 3. В store тащат server state ради "единого места"
- 4. Нет правил для асинхронной логики
- Производительность: где Zustand реально помогает
- Best practices для Zustand в реальном проекте
- Частые ошибки
- Считать Zustand универсальным ответом на любой shared state
- Выбирать Zustand только потому, что Redux кажется "старым"
- Хранить все UI-флаги глобально
- Делать миграцию в Zustand без пересмотра границ состояния
- Как отвечать на интервью
- FAQ
- Zustand лучше Context?
- Zustand лучше Redux Toolkit?
- Можно ли использовать Zustand вместе с Context и server state слоем?
- Стоит ли держать auth в Zustand?
- Когда пора уйти с Zustand на более строгий каркас?
- Итоги
Введение
Вопрос "когда использовать Zustand" почти никогда не сводится к вкусу команды или популярности библиотеки. На практике он появляется в момент, когда локальный useState уже не справляется, Context начинает разрастаться, а полноценный каркас вроде Redux Toolkit пока кажется тяжелым для текущей задачи. Именно в этом промежутке Zustand и дает наибольшую пользу.
Если нужен общая информация по слоям состояния, полезно держать в голове полный разбор state management в React. А если хочется увидеть место Zustand среди альтернатив, стоит ознакомиться с сравнением Redux vs Zustand vs Context в React. Здесь сфокусируемся уже на более узком вопросе: в каких сценариях Zustand действительно дешевле и полезнее других вариантов, а в каких его добавляют слишком рано.
Ключевая мысль простая: Zustand хорош не потому, что он "легкий", а потому что он закрывает конкретный класс проблем. Это разделяемое client state с частыми обновлениями, селективными подписками и невысокой ценой старта. Как только задача выходит за эти границы, решение может оказаться не лучшим.
Больше вопросов в Telegram
Ежедневные разборы и реальные кейсы с интервью
Короткий ответ для практики
Zustand обычно стоит использовать, когда одновременно выполняются четыре условия:
- состояние читают несколько удаленных друг от друга компонентов;
- обновления происходят достаточно часто, чтобы широкий
Contextначал ререндерить лишнее; - логика уже неудобна для локального
useState, но еще не требует жесткой дисциплины Redux Toolkit; - команде нужен быстрый старт без большого количества boilerplate.
Типичные примеры:
- корзина и избранное в e-commerce;
- фильтры, сортировка и локальные пользовательские предпочтения в каталоге;
- состояния сайдбаров, модалок, командной панели и других cross-page UI-элементов;
- клиентские workflow: multi-step flow, draft state, optimistic UI, tab/session shell;
- небольшое доменное состояние в продуктовой фиче, которое уже вышло за пределы одного subtree.
Не стоит автоматически тянуть Zustand туда, где достаточно local state, узкого Context или отдельного слоя server state. Эта граница особенно важна, если вы уже читали материал про client state vs server state в React: store не должен подменять собой query cache и наоборот.
Как понять, что Zustand уже нужен
Самый надежный маркер не "у нас появился shared state", а сочетание трех признаков.
Состояние живет дольше одного компонента
Если данные переживают размонтирование отдельных узлов, читаются в header, sidebar, main-content и еще нужны action-кнопкам в модалке, локальный useState начинает ломать границы ответственности. Подъем состояния выше по дереву теоретически решает проблему, но ценой усложнения props-flow и роста связности.
Обновления частые, а Context начинает ререндерить лишнее
Context отлично работает как механизм доставки данных, но хуже подходит для "горячего" состояния. Когда в одном provider лежат поисковая строка, открытые панели, активные фильтры и UI-флаги, любые изменения начинают задевать слишком широкий круг подписчиков. Подробно эта граница разобрана в статье когда использовать React Context API, а Zustand как раз полезен в момент, когда хочется более точечных подписок на срезы.
Нужно разделяемое client state, а не данные сервера
Если вы храните в store ответ API, который должен инвалидироваться, перезапрашиваться и синхронизироваться с бэкендом, проблема, скорее всего, не в отсутствии Zustand. В таком месте полезнее пересмотреть архитектуру server state. Об этом хорошо напоминает разбор React Query под капотом: query cache и клиентский store решают разные задачи.
Архитектурный разбор: где Zustand лежит в схеме приложения
Контекст задачи
Представим продуктовый кабинет с тремя экранами:
- каталог документов;
- панель фильтров и сортировки;
- правый inspector с деталями выбранного элемента;
- глобальная командная палитра;
- sticky-header, который показывает счетчики и текущий режим интерфейса.
Часть состояния здесь локальная: открыт ли один конкретный dropdown. Часть серверная: список документов и их детали. Но есть и слой, который живет между ними:
- выбранные локальные фильтры до применения;
- текущий режим просмотра;
- pinned-элементы;
- состояние командной палитры;
- client-side selection между несколькими зонами экрана.
Это как раз хорошая зона для Zustand.
Схема компонентов и ролей
Рабочее разделение обычно выглядит так:
useStateхранит короткоживущее состояние конкретного компонента;- URL хранит то, что должно переживать reload и share;
- server state слой владеет данными API;
Contextпередает тему, локаль, auth-shell и инфраструктурные зависимости;- Zustand хранит координирующее client state, которое читается из разных мест и часто меняется.
Такое разделение уменьшает смешение ролей. Если же тащить все в один store, быстро появляется путаница: часть полей должна жить в URL, часть на сервере, часть вообще должна умереть вместе с компонентом.
Поток события
Сценарий "пользователь выбирает документ и открывает inspector" в такой схеме выглядит так:
- Компонент списка пишет
selectedIdв Zustand store. - Inspector читает только
selectedIdиisInspectorOpen. - Server state слой получает новый ключ запроса и запрашивает детали документа.
- Header читает только счетчик pinned-элементов и не зависит от деталей документа.
- Остальные части экрана не ререндерятся, если не подписаны на изменившийся slice.
Здесь главный выигрыш не в количестве строк кода, а в радиусе обновлений. Если та же схема сделана через широкий Context, лишние подписчики получают обновление чаще, чем им нужно. Если сделать ее через Redux Toolkit, архитектура тоже будет рабочей, но выше, чем оправдано масштабом текущей задачи.
Узкие места и точки отказа
Обычно система ломается в трех местах:
- в Zustand начинают складывать server state;
- store превращают в свалку разнородных UI-флагов без доменных границ;
- компоненты читают весь store целиком вместо селективных подписок.
То есть, проблема редко в библиотеке как таковой. Она почти всегда в том, что Zustand воспринимают как "легкий глобальный store для всего".
Код-пример 1: сценарий, где Zustand действительно уместен
Ниже пример общего store для интерфейсной координации каталога. Это не данные сервера, а client state, которое нужно нескольким зонам экрана.
import { create } from "zustand";
type ViewMode = "table" | "board";
type CatalogUiState = {
selectedId: string | null;
pinnedIds: string[];
viewMode: ViewMode;
isInspectorOpen: boolean;
selectItem: (id: string) => void;
togglePin: (id: string) => void;
setViewMode: (mode: ViewMode) => void;
closeInspector: () => void;
};
export const useCatalogUiStore = create<CatalogUiState>((set) => ({
selectedId: null,
pinnedIds: [],
viewMode: "table",
isInspectorOpen: false,
selectItem: (id) =>
set(() => ({
selectedId: id,
isInspectorOpen: true,
})),
togglePin: (id) =>
set((state) => ({
pinnedIds: state.pinnedIds.includes(id)
? state.pinnedIds.filter((itemId) => itemId !== id)
: [...state.pinnedIds, id],
})),
setViewMode: (mode) => set(() => ({ viewMode: mode })),
closeInspector: () => set(() => ({ isInspectorOpen: false })),
}));
Компоненты при этом подписываются только на нужные поля:
function CatalogToolbar() {
const viewMode = useCatalogUiStore((state) => state.viewMode);
const setViewMode = useCatalogUiStore((state) => state.setViewMode);
return (
<div>
<button
onClick={() => setViewMode("table")}
aria-pressed={viewMode === "table"}
>
Table
</button>
<button
onClick={() => setViewMode("board")}
aria-pressed={viewMode === "board"}
>
Board
</button>
</div>
);
}
function Inspector() {
const selectedId = useCatalogUiStore((state) => state.selectedId);
const isOpen = useCatalogUiStore((state) => state.isInspectorOpen);
if (!isOpen || !selectedId) return null;
return <aside>Details for {selectedId}</aside>;
}
Почему здесь Zustand на месте:
- состояние не серверное;
- его читают несколько удаленных компонентов;
- обновления частые и локальные по смыслу;
- селективные подписки уменьшают лишние ререндеры.
Код-пример 2: сценарий, где Zustand уже выглядит сомнительно
Теперь пример, где store используют не по назначению и начинают дублировать server state:
import { create } from "zustand";
type User = {
id: string;
name: string;
role: string;
};
type UsersStore = {
users: User[];
isLoading: boolean;
fetchUsers: () => Promise<void>;
};
export const useUsersStore = create<UsersStore>((set) => ({
users: [],
isLoading: false,
fetchUsers: async () => {
set({ isLoading: true });
const response = await fetch("/api/users");
const users = await response.json();
set({
users,
isLoading: false,
});
},
}));
Технически код рабочий, но архитектурно здесь уже появляются вопросы:
- где стратегия устаревания данных;
- кто отвечает за refetch после мутации;
- как синхронизировать список, детали и счетчики;
- что делать с race conditions и background refresh;
- как избежать второго источника истины, если эти же данные уже есть в query cache.
Именно поэтому полезно регулярно сверяться с материалом client state vs server state и не превращать Zustand в замену инструментам для работы с серверными данными.
Сравнительная таблица: когда использовать Zustand, а когда нет
| Сценарий | Что выбрать по умолчанию | Почему | Когда Zustand все же уместен |
|---|---|---|---|
| Тема, локаль, auth-shell | Context | Редкие обновления и инфраструктурная роль | Если нужны селективные подписки и state стал заметно горячим |
| Модалка или dropdown одного экрана | useState | Состояние короткоживущее и локальное | Если этим управляют несколько удаленных зон UI |
| Multi-step flow внутри одной фичи | useReducer | Явные переходы, но локальная область видимости | Если flow читают и меняют разные части приложения |
| Каталоговые фильтры, selection, view mode | Zustand | Частые обновления и shared client state | Это один из самых естественных кейсов |
| Большая доменная модель нескольких команд | Redux Toolkit | Нужна дисциплина, трассировка и стандартизация | Только если масштаб пока средний и договоренностей достаточно |
| Данные API, кэш запросов, инвалидация | Server state слой | Источник истины на сервере | Лишь для вспомогательных client-side derivations поверх ответа |
Эта таблица хорошо дополняет уже существующий материал когда не нужен Redux: между "локального состояния уже мало" и "нужен строгий Redux-каркас" у Zustand как раз есть сильная практическая ниша.
Прокачай React за 7 дней
20 вопросов и разборов по React Hooks
Production-ловушки: где чаще всего ломают Zustand
1. Store становится свалкой всего подряд
Симптомы:
- в одном store лежат тема, корзина, websocket-статусы, черновики форм и ответы API;
- любое изменение начинает неожиданно задевать соседние фичи;
- новому разработчику сложно понять границы store.
Последствие в production: растет связность и падает предсказуемость изменений. Любой рефакторинг цепляет слишком много кода.
Что делать: делить store по доменным границам, а не по принципу "все глобальное сюда".
2. Компоненты подписываются на слишком широкий кусок
Частая ошибка выглядит так:
function Sidebar() {
const state = useCatalogUiStore();
return <div>{state.viewMode}</div>;
}
Такой компонент подписан на весь store и будет обновляться намного чаще, чем нужно. Проблема особенно болезненна, если команда параллельно пытается разбираться, когда компонент реально перерисовывается в React. Zustand помогает только тогда, когда мы сами используем его селективно.
3. В store тащат server state ради "единого места"
Снаружи это звучит привлекательно, но дальше появляются устаревшие данные, ручная синхронизация и спор о том, какой слой теперь источник истины. В логах и devtools это проявляется как повторная запись одних и тех же сущностей в разные контейнеры.
4. Нет правил для асинхронной логики
У Zustand низкий порог входа, но именно поэтому команда легко недооценивает необходимость договоренностей:
- где живут async-action;
- как обрабатываются ошибки;
- как отменяются устаревшие запросы;
- как тестировать побочные эффекты;
- как делается rollback optimistic UI.
Если этих правил нет, простота старта быстро превращается в хаос сопровождения.
Производительность: где Zustand реально помогает
Главный выигрыш Zustand обычно не в "магической скорости", а в более узком радиусе подписок. Это важно в интерфейсах, где:
- состояние меняется часто;
- подписчиков много;
- часть дерева не должна знать о большинстве обновлений.
Типичный bottleneck здесь не вычисления, а лишние ререндеры и лишняя координация между компонентами. Zustand позволяет читать конкретный slice вместо широкого значения провайдера. Но это работает только при аккуратном проектировании селекторов.
Когда оптимизация оправдана:
- в сложных dashboard-экранах;
- в data-heavy интерфейсах;
- в многооконных UI-shell сценариях;
- там, где
Contextуже заметно расширяет радиус обновлений.
Когда оптимизация преждевременна:
- в маленькой фиче на один экран;
- когда проблема не в client state, а в тяжелых вычислениях или DOM;
- когда store вводят без измерений просто "на всякий случай".
Если производительность действительно под вопросом, полезно параллельно смотреть на profiling React-приложения и не объяснять все проблемы одним выбором state manager.
Best practices для Zustand в реальном проекте
- Держите store маленьким и доменно цельным.
- Не смешивайте в одном слое server state, URL state и локальный UI state без явной причины.
- Подписывайте компоненты только на нужные поля и actions.
- Давайте store имена по бизнес-смыслу:
useCartStore,useEditorUiStore,useSessionShellStore. - Фиксируйте правила async-логики: retry, cancellation, error boundary, optimistic update.
- Добавляйте тесты на переходы состояния в критичных workflow, а не только на happy path.
- Планируйте rollback для optimistic UI заранее, а не после первого инцидента.
- Если проект растет, пересматривайте границы хранения состояния вместе с архитектурой фич. Это хорошо стыкуется с практиками из Feature-Sliced Design для React-проектов.
Частые ошибки
Считать Zustand универсальным ответом на любой shared state
На самом деле сначала нужно определить тип состояния. Иногда ответом будет useState, иногда URL, иногда server state слой, иногда Context, а иногда уже Zustand.
Выбирать Zustand только потому, что Redux кажется "старым"
Это слабый аргумент. Сравнивать нужно не репутацию библиотек, а стоимость сопровождения под конкретный масштаб. Для части систем Redux Toolkit по-прежнему лучше именно из-за дисциплины и tooling. Об этом уже подробно написано в Redux vs Zustand vs Context.
Хранить все UI-флаги глобально
Если модалка живет внутри одной страницы, нет смысла сразу тащить ее в общий store. Глобальное состояние дороже, чем локальное, даже если библиотека очень удобная.
Делать миграцию в Zustand без пересмотра границ состояния
Просто заменить Redux на Zustand или Context на Zustand недостаточно. Если проблема была в неверной модели данных, новая библиотека только сделает ошибку чуть короче по коду.
Как отвечать на интервью
Сильный ответ звучит примерно так:
"Zustand я использую для среднего по сложности client state, когда состояние читают несколько частей приложения, обновления частые, а Context уже дает слишком широкий радиус ререндеров. При этом я не тяну его в server state и не выбираю автоматически для больших доменных систем, где важнее строгая дисциплина и развитые devtools Redux Toolkit."
Чтобы ответ выглядел сильнее, добавьте критерии:
- Где источник истины: в клиенте, URL или на сервере.
- Насколько часто состояние обновляется.
- Сколько зон интерфейса его читают.
- Нужна ли строгая трассировка действий и унификация для команды.
- Какое решение дешевле в сопровождении через полгода, а не только сегодня.
Собеседующий обычно проверяет не любовь к библиотеке, а архитектурное мышление. Если в ответе видно понимание границ Context, цены ререндеров и различия client/server state, это звучит намного сильнее, чем просто "Zustand легче Redux".
Практика технических собеседований по React без случайных вопросов
Тренажер с разбором архитектурных тем: state management, производительность, границы client/server state и аргументация инженерных решений
FAQ
Zustand лучше Context?
Не вообще, а в конкретном классе задач. Context лучше подходит для доставки редко меняющихся инфраструктурных данных. Zustand сильнее там, где нужен shared client state с более точечными подписками.
Zustand лучше Redux Toolkit?
Для средних по сложности задач часто да, потому что старт дешевле. Для больших командных систем не всегда: Redux Toolkit может выиграть за счет дисциплины, стандартизации и зрелого tooling.
Можно ли использовать Zustand вместе с Context и server state слоем?
Да, это очень частая схема. Context передает инфраструктурные зависимости, server state слой отвечает за данные API, а Zustand закрывает координирующее client state.
Стоит ли держать auth в Zustand?
Иногда да, но сначала стоит разделить shell auth-состояние и реальные серверные данные пользователя. Часть auth-задач удобнее решать через Context или специализированный session-layer.
Когда пора уйти с Zustand на более строгий каркас?
Когда store становится слишком большим, команд много, возрастает цена неявных договоренностей и критична трассировка каждой смены состояния. Это сигнал к пересмотру не только библиотеки, но и всей архитектурной дисциплины.
Итоги
Zustand стоит использовать не как модную замену всему, а как инструмент для конкретной ниши: shared client state средней сложности, частые обновления, селективные подписки, быстрый старт и умеренная архитектурная цена. Это его сильная зона.
Если состояние локальное, хватит useState или useReducer. Если данные инфраструктурные и редкие, чаще достаточно Context. Если источник истины на сервере, нужен отдельный server state слой. Если система большая и требует строгой дисциплины, стоит честно сравнить Zustand с Redux Toolkit, а не выбирать по инерции.
Именно поэтому вопрос "когда использовать Zustand" лучше заменять на другой: "какой тип состояния у меня сейчас и сколько будет стоить его сопровождение через полгода". В большинстве реальных проектов правильный ответ начинается именно с этого.
Больше вопросов в Telegram
Ежедневные разборы и реальные кейсы с интервью
Автор
Lexicon Team
Читайте также
frontend
Когда не нужен Redux в React
Разбираем, когда Redux в React избыточен. Когда достаточно local state, useReducer или Context. Почему не стоит смешивать client и server state, как не навредить производительности и какие критерии выбора использовать на практике.
frontend
State management в React: полный разбор
Полный разбор state management в React: local state, Context, useReducer, внешние store, server state, производительность, ошибки и выбор подхода.
frontend
Redux vs Zustand vs Context в React: что выбрать в 2026
Подробное сравнение Redux Toolkit, Zustand и React Context: когда какой подход выбирать, как избежать лишних ререндеров и как аргументировать выбор на техническом собеседовании.