Zustand: простой менеджер состояния для React
Zustand небольшая библиотека для управления глобальным состоянием в React, с минимумом кода, поддержкой middleware и оптимизированными ререндером.
Когда приложение растет, useState
в каждом компоненте начинает раздуваться, а количество ререндеров растет. Часто приходиться писать контексты или подключать Redux. В таких случаях удобно использовать zustand легкую библиотеку, позволяющую держать глобальный стейт в пару строк кода.
Что такое zustand?
zustand
(нем. “состояние”) небольшая библиотека для управления состоянием в React. В отличие от Redux, здесь нет экшенов, редьюсеров и оберток. Достаточно объявить обычную функцию, возвращающую объект со свойствами и методами. Библиотека работает через React-hooks, поэтому ее можно подключать к уже существующим компонентам.
Ключевые характеристики
- Минимальный API один
create
и один хукuseStore
. - Мутируемый стиль пишете обычный код, а
zustand
под капотом делает иммутабельные обновления. - Оптимизированные ререндеры компонент обновляется только при изменении выбранных данных.
- Middleware поддержка логирования, персистентности, devtools и пр.
Создание хранилища
Создание store требует одного вызова create
. Внутри описываем начальное состояние и функции-мутаторы.
import create from "zustand";
import { devtools, persist } from "zustand/middleware";
// простой store без middleware
export const useCartStore = create((set) => ({
items: [],
addItem: (item) => set((state) => ({ items: [...state.items, item] })),
removeItem: (id) =>
set((state) => ({ items: state.items.filter((i) => i.id !== id) })),
}));
// store c devtools и persist
export const usePersistedCart = create(
devtools(
persist(
(set) => ({
items: [],
addItem: (item) => set((state) => ({ items: [...state.items, item] })),
clear: () => set({ items: [] }),
}),
{ name: "cart-storage" }
)
)
);
Первая версия хранит массив items
и два метода. Вторая добавляет middleware: devtools
позволяет просматривать состояние в Redux DevTools, persist
сохраняет данные в localStorage
.
Использование в компонентах
Хук useCartStore
возвращает нужные части стейта. При использовании селектора компонент подписывается только на выбранные данные.
import React from "react";
import { useCartStore } from "./cartStore";
function Cart() {
const items = useCartStore((state) => state.items);
const removeItem = useCartStore((state) => state.removeItem);
return (
<ul>
{items.map((item) => (
<li key={item.id}>
{item.name} {item.price}$
<button onClick={() => removeItem(item.id)}>Удалить</button>
</li>
))}
</ul>
);
}
Каждый вызов useCartStore
с отдельным селектором создает независимую подписку, поэтому изменение items
не заставит перерисовываться компоненты, которые используют только addItem
.
Если нужны сразу несколько полей, их можно собрать в один объект:
const { items, addItem } = useCartStore((state) => ({
items: state.items,
addItem: state.addItem,
}));
Продвинутая работа: shallow и middleware
Сравнение по shallow
По умолчанию сравнение происходит по ссылке. При передаче массивов или объектов каждый ререндер будет срабатывать. Для избежания этого подключаем shallow
из zustand/shallow
.
import shallow from "zustand/shallow";
const [items, total] = useCartStore(
(state) => [state.items, state.items.reduce((sum, i) => sum + i.price, 0)],
shallow
);
shallow
сравнивает массивы и объекты по поверхностным полям, перерисовывая компонент только при реальном изменении.
Пример middleware: логирование
import { subscribeWithSelector } from "zustand/middleware";
import create from "zustand";
export const useLogStore = create(
subscribeWithSelector((set) => ({
count: 0,
inc: () => set((state) => ({ count: state.count + 1 })),
}))
);
useLogStore.subscribe(
(state) => state.count,
(count) => console.log("count changed:", count)
);
subscribeWithSelector
позволяет подписаться на конкретные части состояния и выполнить произвольный код при их изменении.
Сравнительная таблица
Фреймворк | Размер пакета | Boilerplate | DevTools | Persist | Подходит для больших приложений |
---|---|---|---|---|---|
Zustand | ~2 KB | Минимальный | ✅ (middleware) | ✅ (middleware) | Да |
Redux Toolkit | ~10 KB | Средний | ✅ | ✅ | Да |
Context API | Нет | ❌ | ❌ | Маленькие проекты |
Когда стоит выбрать zustand
- Нужно быстро добавить глобальный стейт без экшенов и редьюсеров.
- Приложение уже использует React-hooks, а дополнительный слой управления нежелателен.
- Требуется контроль ререндеров через селекторы и
shallow
. - Нужно подключить devtools или персистентность без отдельной конфигурации.
Чеклист
- Храните в стейте только данные; методы объявляйте в
create
. - Выбирайте минимальный набор полей в селекторах.
- При работе с массивами/объектами используйте
shallow
. - В продакшене ограничьте middleware, которые логируют каждый апдейт.
- Тестируйте store отдельно это обычный объект JavaScript.