Настройка State Management (Zustand) для React-приложения
Zustand — минималистичная библиотека управления состоянием для React. Никакого бойлерплейта, никаких провайдеров, никакого контекста. Стор — это хук, который работает вне дерева компонентов и не вызывает лишних ре-рендеров.
Подходит для большинства SPA среднего размера, где Redux избыточен, а встроенного useState уже недостаточно.
Что входит в работу
Установка и настройка Zustand под конкретный проект: структура сторов, типизация TypeScript, middleware, интеграция с devtools, персистентность, разбивка на слайсы при необходимости.
Установка
npm install zustand
# или
pnpm add zustand
Никаких peer-зависимостей. Размер пакета — около 1 КБ gzipped.
Базовый стор
import { create } from 'zustand'
interface CartState {
items: CartItem[]
total: number
addItem: (item: CartItem) => void
removeItem: (id: string) => void
clearCart: () => void
}
export const useCartStore = create<CartState>((set, get) => ({
items: [],
total: 0,
addItem: (item) =>
set((state) => {
const items = [...state.items, item]
return { items, total: items.reduce((sum, i) => sum + i.price, 0) }
}),
removeItem: (id) =>
set((state) => {
const items = state.items.filter((i) => i.id !== id)
return { items, total: items.reduce((sum, i) => sum + i.price, 0) }
}),
clearCart: () => set({ items: [], total: 0 }),
}))
Использование в компоненте — просто хук:
function CartButton() {
const { items, addItem } = useCartStore()
// компонент ре-рендерится только если items или addItem изменились
return <button onClick={() => addItem(product)}>В корзину ({items.length})</button>
}
Селекторы и оптимизация ре-рендеров
По умолчанию компонент подписывается на весь стор. Чтобы ограничить подписку — передаём селектор:
// ре-рендер только при изменении total
const total = useCartStore((state) => state.total)
// несколько значений — через shallow comparison
import { useShallow } from 'zustand/react/shallow'
const { items, clearCart } = useCartStore(
useShallow((state) => ({ items: state.items, clearCart: state.clearCart }))
)
Middleware: devtools + persist
import { create } from 'zustand'
import { devtools, persist } from 'zustand/middleware'
export const useAuthStore = create<AuthState>()(
devtools(
persist(
(set) => ({
user: null,
token: null,
login: (user, token) => set({ user, token }, false, 'auth/login'),
logout: () => set({ user: null, token: null }, false, 'auth/logout'),
}),
{
name: 'auth-storage',
// сохраняем только token, не весь стор
partialize: (state) => ({ token: state.token }),
}
),
{ name: 'AuthStore' }
)
)
devtools подключает стор к Redux DevTools Extension — видны все экшены и state diff. persist сохраняет состояние в localStorage (или любое другое хранилище через параметр storage).
Разбивка на слайсы
При росте приложения один большой стор становится неудобным. Паттерн слайсов:
// slices/userSlice.ts
import type { StateCreator } from 'zustand'
export interface UserSlice {
user: User | null
setUser: (user: User) => void
}
export const createUserSlice: StateCreator<
UserSlice & CartSlice,
[],
[],
UserSlice
> = (set) => ({
user: null,
setUser: (user) => set({ user }),
})
// store/index.ts
export const useBoundStore = create<UserSlice & CartSlice>()((...args) => ({
...createUserSlice(...args),
...createCartSlice(...args),
}))
Асинхронные действия
Zustand не имеет встроенной обработки async — просто пишем async функции:
interface ProductState {
products: Product[]
loading: boolean
error: string | null
fetchProducts: (categoryId: string) => Promise<void>
}
export const useProductStore = create<ProductState>((set) => ({
products: [],
loading: false,
error: null,
fetchProducts: async (categoryId) => {
set({ loading: true, error: null })
try {
const data = await api.get<Product[]>(`/categories/${categoryId}/products`)
set({ products: data, loading: false })
} catch (err) {
set({ error: err instanceof Error ? err.message : 'Ошибка загрузки', loading: false })
}
},
}))
Подписка вне компонентов
Zustand позволяет читать и изменять стор вне React:
// в сервисе, утилите, роутере
const token = useAuthStore.getState().token
useAuthStore.subscribe(
(state) => state.token,
(token) => {
if (!token) router.navigate('/login')
}
)
Структура файлов
src/
stores/
auth.store.ts
cart.store.ts
ui.store.ts # модальные окна, тема, sidebar
index.ts # реэкспорт всех сторов
Что делаем
Анализируем существующее состояние в приложении (локальный стейт, контекст, props drilling), выносим в сторы, настраиваем devtools и persist там, где нужно, покрываем тестами через zustand/vanilla. Итог — предсказуемая структура данных без лишних зависимостей.
Срок: 1–2 дня в зависимости от объёма существующей кодовой базы.







