Управление состоянием в 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 и интегрировать визуализации в существующие приложения. Записаться
lear
Только профита по сравнению с mobx нет.
С zustand придется костылями пользоваться для того, чтобы создать локальный стейт.
markelov69
Факт