Антон Ларичев

Введение
Управление состоянием — одна из ключевых задач в любом React-приложении. Долгое время Redux был стандартом де-факто, но сегодня у него появился серьёзный конкурент — Zustand. Оба инструмента решают одну задачу, но делают это принципиально по-разному.
В этой статье мы разберём архитектуру каждой библиотеки, сравним их на практических примерах и поможем вам принять обоснованное решение для вашего проекта.
Архитектура и философия
Redux Toolkit
Redux Toolkit (RTK) — это официальный набор инструментов для работы с Redux. Он построен на принципах Flux-архитектуры: единственный глобальный store, иммутабельные обновления через reducers и строгий однонаправленный поток данных.
RTK значительно упрощает работу с Redux: убирает шаблонный код, встраивает Immer для мутабельных обновлений и предоставляет RTK Query для работы с API.
Zustand
Zustand — минималистичная библиотека на основе хуков. Она не навязывает строгую архитектуру: вы создаёте store как обычный JavaScript-объект с состоянием и действиями. Под капотом используется паттерн наблюдателя и контекст React не нужен вовсе.
Сравнение на практике
Установка и начало работы
# Redux Toolkit
npm install @reduxjs/toolkit react-redux
# Zustand
npm install zustand
Создание хранилища: Redux Toolkit
import { createSlice, configureStore, PayloadAction } from '@reduxjs/toolkit';
// Определяем тип состояния
interface CounterState {
value: number;
}
const initialState: CounterState = { value: 0 };
// Создаём slice с редьюсерами и экшенами
const counterSlice = createSlice({
name: 'counter',
initialState,
reducers: {
increment: (state) => {
state.value += 1; // Immer позволяет писать мутации
},
decrement: (state) => {
state.value -= 1;
},
setValue: (state, action: PayloadAction<number>) => {
state.value = action.payload;
},
},
});
export const { increment, decrement, setValue } = counterSlice.actions;
// Настраиваем глобальный store
export const store = configureStore({
reducer: {
counter: counterSlice.reducer,
},
});
export type RootState = ReturnType<typeof store.getState>;
export type AppDispatch = typeof store.dispatch;
// Провайдер оборачивает всё приложение
import { Provider } from 'react-redux';
import { store } from './store';
function App() {
return (
<Provider store={store}>
<Counter />
</Provider>
);
}
import { useDispatch, useSelector } from 'react-redux';
import { increment, decrement } from './counterSlice';
import { RootState } from './store';
function Counter() {
const value = useSelector((state: RootState) => state.counter.value);
const dispatch = useDispatch();
return (
<div>
<span>{value}</span>
<button onClick={() => dispatch(increment())}>+</button>
<button onClick={() => dispatch(decrement())}>-</button>
</div>
);
}
Создание хранилища: Zustand
import { create } from 'zustand';
// Типизируем store
interface CounterStore {
value: number;
increment: () => void;
decrement: () => void;
setValue: (val: number) => void;
}
// Создаём store — всё в одном месте
const useCounterStore = create<CounterStore>((set) => ({
value: 0,
increment: () => set((state) => ({ value: state.value + 1 })),
decrement: () => set((state) => ({ value: state.value - 1 })),
setValue: (val) => set({ value: val }),
}));
// Никаких провайдеров не нужно — используем хук напрямую
function Counter() {
const { value, increment, decrement } = useCounterStore();
return (
<div>
<span>{value}</span>
<button onClick={increment}>+</button>
<button onClick={decrement}>-</button>
</div>
);
}
Асинхронные операции
RTK предоставляет createAsyncThunk для работы с API:
import { createAsyncThunk } from '@reduxjs/toolkit';
// Асинхронный thunk для загрузки данных пользователя
export const fetchUser = createAsyncThunk(
'user/fetchById',
async (userId: string) => {
const response = await fetch(`/api/users/${userId}`);
return response.json();
}
);
В Zustand асинхронные действия — просто async-функции:
interface UserStore {
user: User | null;
loading: boolean;
fetchUser: (id: string) => Promise<void>;
}
const useUserStore = create<UserStore>((set) => ({
user: null,
loading: false,
// Асинхронное действие — обычная async-функция
fetchUser: async (id) => {
set({ loading: true });
const response = await fetch(`/api/users/${id}`);
const user = await response.json();
set({ user, loading: false });
},
}));
Производительность и селекторы
RTK использует reselect для мемоизированных селекторов:
import { createSelector } from '@reduxjs/toolkit';
// Мемоизированный селектор — пересчитывается только при изменении зависимостей
const selectFilteredItems = createSelector(
(state: RootState) => state.items.list,
(state: RootState) => state.items.filter,
(list, filter) => list.filter(item => item.category === filter)
);
Zustand решает проблему лишних перерендеров через подписку на конкретные части состояния:
// Компонент перерендерится только при изменении поля value
const value = useCounterStore((state) => state.value);
Когда выбирать Redux Toolkit
- Крупное enterprise-приложение с большой командой
- Нужны мощные DevTools для отладки и time-travel debugging
- Сложная бизнес-логика с множеством взаимозависимых состояний
- Уже используете RTK Query для управления серверным состоянием
- Требуется строгая архитектура и единые соглашения в команде
Когда выбирать Zustand
- Небольшие или средние проекты
- Нужно быстро стартовать без boilerplate-кода
- Небольшая команда или соло-разработка
- Хотите минимальный бандл (Zustand весит около 1 КБ)
- Нужно локальное состояние нескольких компонентов без глобального store
Частые ошибки
Ошибка 1: Подписка на весь store в Zustand
// Плохо: компонент перерендерится при любом изменении store
const store = useCounterStore();
// Хорошо: подписываемся только на нужное поле
const value = useCounterStore((state) => state.value);
Ошибка 2: Мутация состояния в Redux напрямую
// Плохо: прямая мутация вне Immer-контекста вызовет ошибки
const badReducer = (state, action) => {
state.items.push(action.payload); // ошибка вне createSlice
return state;
};
// Хорошо: используем createSlice — Immer встроен
const goodSlice = createSlice({
name: 'items',
initialState: { items: [] },
reducers: {
addItem: (state, action) => {
state.items.push(action.payload); // безопасно внутри createSlice
},
},
});
Ошибка 3: Хранить серверные данные в Zustand вместо React Query
Zustand предназначен для клиентского состояния (UI, фильтры, модалки). Для серверных данных лучше использовать React Query или RTK Query — они берут на себя кэширование, дедупликацию запросов и инвалидацию.
Ошибка 4: Один огромный store в Zustand
// Плохо: всё в одном store — сложно поддерживать
const useAppStore = create(() => ({
user: null,
cart: [],
theme: 'dark',
notifications: [],
}));
// Хорошо: разделяем по доменам
const useUserStore = create(...);
const useCartStore = create(...);
const useThemeStore = create(...);
Заключение
Zustand и Redux Toolkit — отличные инструменты, каждый со своей нишей. Zustand побеждает в простоте, скорости разработки и размере бандла. Redux Toolkit — в структурированности, экосистеме и инструментах отладки.
Для большинства новых проектов мы рекомендуем начинать с Zustand и переходить на RTK только тогда, когда сложность приложения этого действительно требует. Помните: лучший инструмент — тот, который решает вашу конкретную задачу с минимальными накладными расходами.






Комментарии
0