Управление состоянием в React — один из самых важных моментов при разработке приложений. Многие начинают с useState и useReducer, но со временем понимают, что для глобального состояния нужно что‑то более мощное. Здесь хорошо подойдут Redux, MobX, Recoil и, конечно, Zustand.

Zustand (читается «цуштанд», в переводе с немецкого — «состояние») — это простая и мощная библиотека для управления состоянием в React, которая решает проблемы существующих решений:

  • Нет бойлерплейта — минимальный код по сравнению с Redux.

  • Нет контекста — Zustand не требует Context.Provider, избавляя от лишних ререндеров.

  • Высокая производительность — ререндер происходит только при изменении подписанных частей состояния.

  • Гибкость — можно использовать Zustand как для глобального состояния, так и для локального.


Создание базового хранилища

В Zustand хранилище — это обычный хук, который возвращает состояние и экшены для его изменения. Реализуем счетчик.

Создадим простое хранилище с count и экшенами increment и reset:

import { create } from 'zustand';

// создаём хранилище Zustand
const useStore = create((set) => ({
  count: 0,
  increment: () => set((state) => ({ count: state.count + 1 })),
  reset: () => set({ count: 0 }),
}));
  • set — функция для изменения состояния.

  • state.count + 1 — обновляем состояние на основе предыдущего.

  • reset — сбрасывает счетчик в 0.

Теперь подключим Zustand в React‑компонент:

function Counter() {
  const { count, increment, reset } = useStore();
  
  return (
    <div>
      <h1>{count}</h1>
      <button onClick={increment}>+1</button>
      <button onClick={reset}>Сброс</button>
    </div>
  );
}

Zustand не требует ни useReducer, ни контекста, ни Redux — все понятно.

Как избежать лишних ререндеров?

Одна из главных фич Zustand — селекторы, позволяющие подписываться на отдельные части состояния.

Если использовать useStore() без параметров, компонент будет перерисовываться при любом изменении хранилища.

function BadComponent() {
  const store = useStore(); // будет ререндериться на любое изменение состояния
  return <p>{store.count}</p>;
}

Используем селектор, подписываясь только на count:

function GoodComponent() {
  const count = useStore((state) => state.count); // перерисовывается только при изменении count
  return <p>{count}</p>;
}

Плюс можно использовать useShallow, чтобы подписываться на несколько значений без лишних ререндеров:

import { useShallow } from 'zustand/react/shallow';

const { name, age } = useUserStore(
  useShallow((state) => ({ name: state.name, age: state.age }))
);

Асинхронные действия

Допустим, у нас есть API https://jsonplaceholder.typicode.com/users, из которого хочется хотим загружать пользователей.

Создадим Zustand‑хранилище:

const useUserStore = create((set) => ({
  users: [],
  fetchUsers: async () => {
    const response = await fetch('https://jsonplaceholder.typicode.com/users');
    const data = await response.json();
    set({ users: data });
  },
}));

fetchUsers делает fetch, получает JSON и обновляет users в хранилище, после этого set({ users: data }) полностью заменяет состояние.

Используем в компоненте:

function UserList() {
  const { users, fetchUsers } = useUserStore();

  useEffect(() => {
    fetchUsers();
  }, []);

  return (
    <ul>
      {users.map((user) => (
        <li key={user.id}>{user.name}</li>
      ))}
    </ul>
  );
}

Теперь при монтировании компонент загружает пользователей и обновляет состояние.

Zustand без React

Zustand позволяет работать вне React, например, для WebSockets. Например:

import { createStore } from 'zustand/vanilla';

const store = createStore((set) => ({
  count: 0,
  increment: () => set((state) => ({ count: state.count + 1 })),
}));

// Доступ к состоянию
store.getState().count; 
store.setState({ count: 10 });

Middleware в Zustand

Zustand поддерживает middleware.

  • persist — сохранение состояния в localStorage

  • immer — удобная работа с вложенными объектами

  • devtools — интеграция с Redux DevTools

persist

Чтобы сохранить состояние между перезапусками браузера:

import { persist } from 'zustand/middleware';

const useStore = create(
  persist(
    (set) => ({
      count: 0,
      increment: () => set((state) => ({ count: state.count + 1 })),
    }),
    { name: 'counter-storage' }
  )
);

Теперь count сохраняется в localStorage.

immer

Если нужно обновлять вложенные объекты без лишних мутаций:

import { immer } from 'zustand/middleware/immer';

const useStore = create(
  immer((set) => ({
    user: { name: 'Alice', age: 25 },
    updateName: (name) =>
      set((state) => {
        state.user.name = name;
      }),
  }))
);

Redux DevTools

Zustand интегрируется с Redux DevTools, позволяя отслеживать изменения:

import { devtools } from 'zustand/middleware';

const useStore = create(
  devtools((set) => ({
    count: 0,
    increment: () => set((state) => ({ count: state.count + 1 })),
  }))
);

Разделение хранилища на слайсы

Для крупных приложений состояние можно разделять на слайсы:

const createUserSlice = (set) => ({
  user: { name: 'Alice', age: 25 },
  updateUser: (newUser) => set({ user: newUser }),
});

const createAuthSlice = (set) => ({
  isLoggedIn: false,
  login: () => set({ isLoggedIn: true }),
  logout: () => set({ isLoggedIn: false }),
});

const useStore = create((...a) => ({
  ...createUserSlice(...a),
  ...createAuthSlice(...a),
}));

Подробнее с zustand можно ознакомиться здесь.

Не могу не упомянуть об открытых уроках по React, которые пройдут скоро в Otus:

  • 6 февраля: «Что нового в React 19?». На уроке вас ждет обзор новых хуков и возможностей создания новых пользовательских элементов. Записаться

  • 20 февраля: «React и графические библиотеки: визуализация данных». Узнаете, как визуализировать данные в React и интегрировать визуализации в существующие приложения. Записаться

Комментарии (2)


  1. lear
    04.02.2025 11:09

    Только профита по сравнению с mobx нет.

    С zustand придется костылями пользоваться для того, чтобы создать локальный стейт.


    1. markelov69
      04.02.2025 11:09

      Факт