/
Блог Хекслета
/
Код
/

Zustand: управление состоянием в React — Хекслет

Zustand: управление состоянием в React — Хекслет

25 июня 2026 г.

8 минут
2
Zustand: управление состоянием в React — Хекслет

Zustand: управление состоянием в React — без шаблонного кода

Знакомая сцена. Вы пишете React-приложение, и сначала всё просто: данные живут в компоненте через useState. Потом эти данные понадобились соседнему компоненту, потом ещё одному уровнем выше. Начинается проброс через пропсы: значение идёт через пять компонентов, которым оно не нужно, только чтобы добраться до шестого. Код распухает, а каждое переименование задевает половину дерева.

Вы вспоминаете про Context — и натыкаетесь на его особенность: при любом изменении значения перерисовываются все компоненты, которые к нему подключены, даже если им важна совсем другая часть данных. Тогда приходит мысль про Redux. Открываете — и видите гору обвязки: типы действий, создатели действий, редьюсеры, диспетчер, обёртка-провайдер вокруг приложения. Чтобы увеличить счётчик на единицу, нужно написать четыре файла.

Zustand убирает эту обвязку целиком. Общее состояние описывается одним хранилищем в несколько строк: данные и функции их менять — рядом. Любой компонент берёт из хранилища ровно то, что ему нужно, и перерисовывается, только когда меняется именно эта часть. Ни проброса пропсов, ни провайдеров, ни лишних перерисовок. Это разница между «полдня на настройку Redux» и «хранилище готово за пять минут».

Разберём по шагам: как создать хранилище, как брать из него данные, почему не нужен шаблонный код и чем Zustand отличается от Redux.

Zustand работает поверх React — сам React и подходы к управлению состоянием, включая Redux Toolkit, разбирают в программе «Фронтенд-разработчик» на Хекслете.

Что такое Zustand

Zustand — небольшая библиотека для управления общим состоянием в React. Слово переводится с немецкого как «состояние». Её задача — хранить данные, которые нужны разным компонентам, и давать удобно их читать и менять, без обвязки, которой славятся другие решения.

Главная идея — одно хранилище, которое одновременно служит хуком. Вы описываете в нём данные и функции, которые эти данные меняют. Дальше любой компонент вызывает этот хук и получает доступ к состоянию. Не нужен провайдер вокруг приложения, не нужен диспетчер, не нужно делить логику на действия и редьюсеры. Всё в одном месте и в привычном виде функций.

Zustand набрал популярность как лёгкая замена тяжёлым решениям. Его берут, когда Context уже не хватает из-за лишних перерисовок, а Redux кажется избыточным для задачи. При этом библиотека крошечная и почти не добавляет веса приложению. Вокруг неё выросло активное сообщество, набор готовых middleware и подробная документация с живыми примерами, так что типовую задачу почти всегда удаётся решить без изобретения велосипеда.

Первое хранилище

Хранилище создаётся функцией create. Внутри — объект с данными и функциями, которые их меняют. Функция set обновляет состояние.

zustand_02_anatomy.png
import { create } from 'zustand';

const useCounterStore = create((set) => ({
  count: 0,                                          // данные
  increment: () => set((s) => ({ count: s.count + 1 })),  // действия
  decrement: () => set((s) => ({ count: s.count - 1 })),
  reset: () => set({ count: 0 }),
}));

Главное здесь — данные и действия лежат вместе. Нет отдельного файла с типами, отдельного с редьюсером и отдельного с действиями. Счётчик и три способа его изменить описаны в одном объекте. Результат create — это хук useCounterStore, готовый к использованию в любом компоненте.

Использование в компонентах

В компоненте вызываете хук и берёте из него нужное. Важная деталь: брать стоит не всё состояние целиком, а конкретную часть через функцию-селектор.

function Counter() {
  // берём только то, что нужно этому компоненту
  const count = useCounterStore((s) => s.count);
  const increment = useCounterStore((s) => s.increment);

  return (
    <button onClick={increment}>
      Нажато: {count}
    </button>
  );
}

Зачем выбирать часть, а не брать всё. Компонент перерисовывается только тогда, когда меняется именно та часть состояния, которую он выбрал. Counter подписался на count — он отреагирует на изменение счётчика, но не дрогнет, если в хранилище поменяется что-то другое. Это и решает проблему лишних перерисовок, из-за которой отказываются от Context.

zustand_01_approaches.png

Обратите внимание: никакого провайдера вокруг приложения нет. Хук работает сам по себе, его можно вызвать из любого компонента в любом месте дерева. Состояние общее, а подключение — точечное.

Почему не нужен шаблонный код

Чтобы оценить разницу, посмотрим на один и тот же счётчик в Redux и в Zustand. Redux Toolkit заметно сократил обвязку по сравнению с классическим Redux, но всё равно требует больше церемоний.

zustand_03_vsredux.png

Что нужно

Redux

Zustand

Описать состояние

Срез (slice) с начальным значением

Поле в объекте

Описать изменения

Редьюсеры внутри среза

Функции рядом с данными

Вызвать изменение

Через диспетчер действий

Прямой вызов функции

Подключить к приложению

Обёртка-провайдер вокруг корня

Ничего — хук работает сам

Прочитать в компоненте

Хук-селектор

Хук-селектор

Суть отличия в подходе. Redux строится на строгой схеме: состояние меняется только через действия, которые проходят через редьюсеры, — это даёт предсказуемость и удобную отладку на больших проектах. Zustand отказывается от обязательной церемонии: вы просто вызываете функцию, которая меняет состояние. Меньше правил — меньше кода, но и меньше встроенной строгости. Для большинства приложений этот размен оказывается выгодным.

Селекторы и перерисовки

Главная техническая ценность Zustand — точечные перерисовки. Разберём, как это работает, потому что здесь кроется и польза, и частая ошибка.

zustand_04_selectors.png

Когда компонент берёт из хранилища срез через селектор, Zustand запоминает, на что именно тот подписался. При изменении состояния перерисовываются только те компоненты, чей срез действительно поменялся. Один компонент следит за счётчиком, другой — за списком задач, и они не мешают друг другу.

// этот компонент перерисуется только при смене count
const count = useStore((s) => s.count);

// а этот — только при смене user
const user = useStore((s) => s.user);

// этот берёт всё состояние — перерисуется на ЛЮБОЕ изменение (так не надо)
const everything = useStore();

Селектор не должен создавать новый объект каждый раз. Если в селекторе писать (s) => ({ a: s.a, b: s.b }), он возвращает новый объект при каждой проверке, и Zustand считает, что состояние «изменилось» — компонент перерисовывается без нужды. Выбирайте отдельные поля по одному либо используйте специальную функцию сравнения. Это самая частая ошибка новичков в Zustand.

Middleware: возможности поверх хранилища

Базовое хранилище умеет хранить и менять данные. Дополнительные возможности подключают через middleware — обёртки вокруг хранилища. Самые полезные три.

zustand_05_middleware.png

Middleware

Что добавляет

persist

Сохраняет состояние в браузере и восстанавливает после перезагрузки страницы

devtools

Подключает к Redux DevTools — видно историю изменений состояния

immer

Даёт менять вложенные данные как обычные объекты, не копируя руками

Самый востребованный — persist. Он сам сохраняет состояние в браузере, и после перезагрузки страницы корзина или настройки остаются на месте. Подключается обёрткой вокруг хранилища:

import { create } from 'zustand';
import { persist } from 'zustand/middleware';

const useCartStore = create(
  persist(
    (set) => ({
      items: [],
      addItem: (item) => set((s) => ({ items: [...s.items, item] })),
    }),
    { name: 'cart' }   // ключ, под которым состояние ляжет в браузере
  )
);

Несколько middleware можно вкладывать друг в друга, выстраивая цепочку: например, и сохранять состояние, и подключить отладку одновременно.

Доступ вне компонентов

Иногда состояние нужно прочитать или изменить там, где хуков нет: в обычной функции, в обработчике, в коде запроса к серверу. Хранилище Zustand умеет и это — у него есть методы помимо хука.

// прочитать текущее состояние без хука
const count = useCounterStore.getState().count;

// изменить состояние снаружи
useCounterStore.setState({ count: 100 });

// подписаться на изменения вне React
useCounterStore.subscribe((state) => {
  console.log('Состояние изменилось:', state);
});

Это удобно, когда логика живёт за пределами компонентов. Например, после успешного ответа сервера обновить состояние прямо в функции запроса, не пробрасывая туда хуки. С Redux для этого тоже есть инструменты, но в Zustand доступ снаружи устроен проще.

Практика: хранилище списка задач

Соберём общее хранилище для списка задач, которым пользуются несколько компонентов.

Шаг 1. Создать хранилище. Данные и действия в одном месте:

import { create } from 'zustand';

const useTasksStore = create((set) => ({
  tasks: [],
  addTask: (title) =>
    set((s) => ({ tasks: [...s.tasks, { id: Date.now(), title, done: false }] })),
  toggleTask: (id) =>
    set((s) => ({
      tasks: s.tasks.map((t) =>
        t.id === id ? { ...t, done: !t.done } : t
      ),
    })),
  removeTask: (id) =>
    set((s) => ({ tasks: s.tasks.filter((t) => t.id !== id) })),
}));

Шаг 2. Список читает задачи. Берёт только массив задач — перерисуется лишь при его изменении:

function TaskList() {
  const tasks = useTasksStore((s) => s.tasks);
  const toggle = useTasksStore((s) => s.toggleTask);

  return (
    <ul>
      {tasks.map((t) => (
        <li key={t.id} onClick={() => toggle(t.id)}>
          {t.done ? '✓' : '○'} {t.title}
        </li>
      ))}
    </ul>
  );
}

Шаг 3. Форма добавляет задачи. Этот компонент берёт только функцию добавления — и не перерисовывается при изменении самого списка:

function AddTask() {
  const addTask = useTasksStore((s) => s.addTask);
  // форма вызывает addTask(title) при отправке
}

Готово. Два компонента работают с общим состоянием, но подписаны на разные его части: список реагирует на задачи, форма — нет. Ни провайдера, ни диспетчера, ни проброса пропсов. Всё хранилище — один вызов create, и то же самое легко расширяется новыми действиями без переделки компонентов.

Асинхронность: запросы прямо в хранилище

Часто данные приходят с сервера, и их загрузку удобно держать в самом хранилище, а не размазывать по компонентам. В Zustand для этого ничего особенного не нужно — действие может быть асинхронной функцией, а set вызывается после ответа сервера.

const useUsersStore = create((set) => ({
  users: [],
  loading: false,
  fetchUsers: async () => {
    set({ loading: true });
    const res = await fetch('/api/users');
    const users = await res.json();
    set({ users, loading: false });
  },
}));

Компонент просто вызывает fetchUsers() и подписывается на users и loading. Вся логика загрузки — индикатор ожидания, запись результата — собрана в одном месте, рядом с данными. В Redux то же самое требует отдельных средств для асинхронных действий, в Zustand это обычная функция.

Дробим хранилище на части

Когда приложение растёт, единое хранилище разбухает. Есть два пути держать его в порядке. Первый — просто завести несколько отдельных хранилищ по смыслу: одно под задачи, другое под пользователя, третье под корзину. Каждый компонент берёт из нужного.

Второй путь, когда части состояния всё же связаны, — слайсы. Хранилище собирают из нескольких функций-кусочков, каждая описывает свою область, а затем их объединяют в одно. Так логика разнесена по файлам, но состояние остаётся общим.

const createTasksSlice = (set) => ({
  tasks: [],
  addTask: (t) => set((s) => ({ tasks: [...s.tasks, t] })),
});

const createUserSlice = (set) => ({
  user: null,
  setUser: (user) => set({ user }),
});

// собираем хранилище из кусочков
const useStore = create((...a) => ({
  ...createTasksSlice(...a),
  ...createUserSlice(...a),
}));

Выбор между путями простой: не связанные между собой области выносят в отдельные хранилища, связанные — собирают слайсами в одно. И то и другое лучше, чем один гигантский объект на всё приложение.

Когда что выбирать

Zustand хорош, но не отменяет другие инструменты. Выбор зависит от того, насколько состояние общее и сложное.

Ситуация

Чем решать

Состояние внутри одного компонента

useState — общее хранилище не нужно

Редко меняющиеся данные на всё приложение (тема, язык)

Context вполне достаточно

Общее состояние, важна простота

Zustand

Большая команда, сложная логика, строгая отладка

Redux Toolkit оправдан

Грубое правило: локальное держите в useState, редкое глобальное — в Context, активно меняющееся общее — в Zustand. К Redux обращаются, когда нужны строгие правила и развитая отладка на большом проекте с командой.

Антипаттерны

Брать всё состояние вместо среза. Вызов useStore() без селектора подписывает компонент на любое изменение хранилища — и он перерисовывается постоянно. Всегда выбирайте конкретные поля через селектор.

Селектор, возвращающий новый объект. Селектор вида (s) => ({ a: s.a }) создаёт новый объект на каждой проверке, и хранилище считает состояние изменённым. Выбирайте поля по одному или задавайте функцию сравнения.

Класть в хранилище то, что должно быть локальным. Значение поля ввода, открыт ли выпадающий список — это состояние одного компонента, ему место в useState. Общее хранилище для локальных данных только усложняет код.

Один гигантский стор на всё. Когда в одном хранилище свалены задачи, пользователь, настройки и корзина, оно становится непонятным. Логичнее разбить на несколько небольших хранилищ по смыслу.

Менять состояние напрямую, минуя set. Правка данных в обход функции set не вызовет перерисовку — интерфейс не обновится. Состояние меняют только через set.


FAQ

Zustand заменяет Redux?

Для многих проектов — да, особенно для средних и небольших. Но Redux Toolkit остаётся сильным выбором там, где важны строгие правила изменения состояния, развитая отладка и общие договорённости в большой команде. Zustand выигрывает простотой, Redux — строгостью и зрелой экосистемой.

Нужно ли знать Redux перед Zustand?

Нет, Zustand можно освоить первым — он проще. Но понимание идей Redux помогает: что такое общее состояние, почему его меняют предсказуемо, зачем нужны селекторы. Эти понятия общие для обоих инструментов.

Работает ли Zustand вне React?

Да, ядро Zustand не привязано к React. Состояние можно читать и менять через getState и setState в любом коде. Но основная польза библиотеки — именно в связке с React-компонентами через хук.

Как сохранить состояние между перезагрузками?

Через middleware persist. Он сам сохраняет состояние в браузере и восстанавливает его при следующем заходе. Достаточно обернуть хранилище и указать ключ. Так делают корзины, настройки и черновики форм.

Не будет ли всё приложение перерисовываться при изменении хранилища?

Нет, если брать данные через селекторы. Каждый компонент подписывается только на свою часть состояния и реагирует лишь на её изменение. Лишние перерисовки возникают, только если брать всё состояние целиком или возвращать из селектора новый объект.

Подходит ли Zustand для больших приложений?

Да, но с дисциплиной. На большом проекте состояние разбивают на несколько хранилищ по смыслу, аккуратно пишут селекторы и подключают devtools для отладки. Сам по себе размер приложения не мешает — мешает свалить всё в одно огромное хранилище без структуры.

Дружит ли Zustand с TypeScript?

Да, и это одна из причин его популярности. Тип состояния задаётся при создании хранилища, после чего редактор подсказывает поля и действия, а несовпадение типов подсвечивается до запуска. Типизация в Zustand считается удобнее, чем в классическом Redux, хотя и там с Redux Toolkit ситуация заметно улучшилась.

Сколько весит Zustand?

Очень мало — это одна из его сильных сторон. Библиотека крошечная по сравнению с экосистемой Redux и почти не добавляет веса итоговой сборке. Для проектов, где важен размер загружаемого кода, это ощутимый плюс при том же наборе возможностей для управления общим состоянием.

Никита Вихров

2 дня назад

2

+7 800 100 22 47

бесплатно по РФ

+7 495 085 21 62

бесплатно по Москве

108813 г. Москва, вн.тер.г. поселение Московский,
г. Московский, ул. Солнечная, д. 3А, стр. 1, помещ. 20Б/3
ОГРН 1217300010476
ИНН 7325174845