ДокументацияReactZustand — управление состоянием
Средний 10 мин чтения

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

Zustand — минималистичный менеджер состояния для React. Простое глобальное состояние без boilerplate, middleware и провайдеров.

Zustandstate managementReactглобальное состояниеstore

Что такое Zustand

Zustand — библиотека для управления глобальным состоянием в React. Минимальный API, нет провайдеров, нет boilerplate. Состояние хранится в обычном JavaScript-объекте вне дерева компонентов.

Установка:

npm install zustand

Создание store

// src/stores/useCartStore.ts
import { create } from 'zustand'

interface CartItem {
  id: number
  name: string
  price: number
  quantity: number
}

interface CartStore {
  items: CartItem[]
  addItem: (item: Omit<CartItem, 'quantity'>) => void
  removeItem: (id: number) => void
  clearCart: () => void
  totalPrice: () => number
}

export const useCartStore = create<CartStore>((set, get) => ({
  items: [],

  addItem: (item) =>
    set((state) => {
      const existing = state.items.find((i) => i.id === item.id)
      if (existing) {
        return {
          items: state.items.map((i) =>
            i.id === item.id ? { ...i, quantity: i.quantity + 1 } : i,
          ),
        }
      }
      return { items: [...state.items, { ...item, quantity: 1 }] }
    }),

  removeItem: (id) =>
    set((state) => ({
      items: state.items.filter((i) => i.id !== id),
    })),

  clearCart: () => set({ items: [] }),

  totalPrice: () =>
    get().items.reduce((sum, i) => sum + i.price * i.quantity, 0),
}))

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

import { useCartStore } from '@/stores/useCartStore'

function ProductCard({ id, name, price }: { id: number; name: string; price: number }) {
  const addItem = useCartStore((state) => state.addItem)

  return (
    <div className="border rounded p-4">
      <h3>{name}</h3>
      <p>{price}</p>
      <button onClick={() => addItem({ id, name, price })}>
        В корзину
      </button>
    </div>
  )
}
function CartBadge() {
  const items = useCartStore((state) => state.items)

  return <span>{items.reduce((sum, i) => sum + i.quantity, 0)}</span>
}

Селектор (state) => state.items — важная деталь. Компонент ре-рендерится только когда items меняется. Если написать useCartStore() без селектора, компонент будет ре-рендериться при любом изменении store.

Сравнение: Context vs Zustand vs Redux

КритерийContextZustandRedux Toolkit
BoilerplateСреднийМинимумМного
ПровайдерыОбязательныНе нужныОбязательны
Re-renderВсех потребителейПо селекторуПо селектору
DevToolsНетДа (плагин)Да
MiddlewareНетДаДа
Кривая обученияНизкаяНизкаяСредняя

Подписка на часть состояния

Zustand ре-рендерит компонент только если результат селектора изменился:

// ✅ Ре-рендер только при изменении items
const items = useCartStore((state) => state.items)

// ✅ Можно вычислять производные значения
const isEmpty = useCartStore((state) => state.items.length === 0)

// ❌ Компонент ре-рендерится при ЛЮБОМ изменении store
const store = useCartStore()

Для сложных вычислений в селекторе используйте useShallow:

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

function CartSummary() {
  const { items, totalPrice } = useCartStore(
    useShallow((state) => ({
      items: state.items,
      totalPrice: state.totalPrice(),
    })),
  )

  return (
    <div>
      <p>Товаров: {items.length}</p>
      <p>Итого: {totalPrice}</p>
    </div>
  )
}

Persist: сохранение в localStorage

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

interface SettingsStore {
  theme: 'light' | 'dark'
  language: string
  setTheme: (theme: 'light' | 'dark') => void
  setLanguage: (lang: string) => void
}

export const useSettingsStore = create<SettingsStore>()(
  persist(
    (set) => ({
      theme: 'light',
      language: 'ru',
      setTheme: (theme) => set({ theme }),
      setLanguage: (language) => set({ language }),
    }),
    {
      name: 'app-settings',
    },
  ),
)

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

Immer middleware

Для удобного обновления вложенных объектов:

npm install immer
import { create } from 'zustand'
import { immer } from 'zustand/middleware/immer'

interface UserStore {
  profile: {
    name: string
    address: {
      city: string
      street: string
    }
  }
  updateCity: (city: string) => void
}

export const useUserStore = create<UserStore>()(
  immer((set) => ({
    profile: {
      name: 'Иван',
      address: { city: 'Москва', street: 'Тверская' },
    },
    updateCity: (city) =>
      set((state) => {
        state.profile.address.city = city
      }),
  })),
)

Без immer пришлось бы писать set(s => ({ profile: { ...s.profile, address: { ...s.profile.address, city } } })).

Async actions

Zustand работает с асинхронностью без лишних усложнений:

interface PostStore {
  posts: Post[]
  loading: boolean
  error: string | null
  fetchPosts: () => Promise<void>
}

export const usePostStore = create<PostStore>((set) => ({
  posts: [],
  loading: false,
  error: null,

  fetchPosts: async () => {
    set({ loading: true, error: null })
    try {
      const res = await fetch('/api/posts')
      const posts = await res.json()
      set({ posts, loading: false })
    } catch (err) {
      set({ error: (err as Error).message, loading: false })
    }
  },
}))

DevTools

import { devtools } from 'zustand/middleware'

export const useCartStore = create<CartStore>()(
  devtools(
    (set, get) => ({
      // ... store
    }),
    { name: 'CartStore' },
  ),
)

Расширение Redux DevTools покажет все действия и состояние store.

Итог

Zustand — лучший выбор для большинства React-проектов, которым нужно глобальное состояние. Нет провайдеров, нет шаблонного кода, точечные ре-рендеры через селекторы. Persist и DevTools подключаются одной строчкой. Если Redux кажется громоздким — попробуйте Zustand.