Зачем нужен State Management
Когда приложение растёт, передача состояния через props становится неудобной (prop drilling). Библиотеки управления состоянием решают эту проблему, предоставляя глобальный store.
Zustand — простота и минимализм
Zustand — легковесная библиотека (1KB) с простым API:
import { create } from 'zustand';
// Определение store
interface CartStore {
items: CartItem[];
total: number;
addItem: (item: CartItem) => void;
removeItem: (id: string) => void;
clearCart: () => void;
}
const useCartStore = create<CartStore>((set, get) => ({
items: [],
total: 0,
addItem: (item) => set((state) => {
const existingItem = state.items.find(i => i.id === item.id);
if (existingItem) {
return {
items: state.items.map(i =>
i.id === item.id ? { ...i, quantity: i.quantity + 1 } : i
),
total: state.total + item.price
};
}
return {
items: [...state.items, { ...item, quantity: 1 }],
total: state.total + item.price
};
}),
removeItem: (id) => set((state) => {
const item = state.items.find(i => i.id === id);
return {
items: state.items.filter(i => i.id !== id),
total: state.total - (item ? item.price * item.quantity : 0)
};
}),
clearCart: () => set({ items: [], total: 0 })
}));
// Использование в компонентах
function CartButton() {
const itemCount = useCartStore((state) => state.items.length);
return <button>Cart ({itemCount})</button>;
}
function CartTotal() {
const total = useCartStore((state) => state.total);
return <div>Total: {total}₽</div>;
}
function AddToCartButton({ product }: { product: Product }) {
const addItem = useCartStore((state) => state.addItem);
return <button onClick={() => addItem(product)}>Add to Cart</button>;
}
Zustand Middleware
import { create } from 'zustand';
import { persist, devtools } from 'zustand/middleware';
// Persist — сохранение в localStorage
const useCartStore = create<CartStore>()(
devtools(
persist(
(set) => ({
items: [],
total: 0,
addItem: (item) => set((state) => ({ ... })),
}),
{
name: 'cart-storage', // ключ в localStorage
partialize: (state) => ({ items: state.items }), // что сохранять
}
),
{ name: 'CartStore' } // имя в DevTools
)
);
// Immer middleware для иммутабельных обновлений
import { immer } from 'zustand/middleware/immer';
const useStore = create<State>()(
immer((set) => ({
users: [],
addUser: (user) => set((state) => {
state.users.push(user); // Мутация безопасна с immer
}),
}))
);
Redux Toolkit — стандарт индустрии
Redux Toolkit упрощает работу с Redux:
import { createSlice, configureStore, PayloadAction } from '@reduxjs/toolkit';
// Slice — часть состояния
const cartSlice = createSlice({
name: 'cart',
initialState: {
items: [] as CartItem[],
total: 0
},
reducers: {
addItem: (state, action: PayloadAction<CartItem>) => {
const existingItem = state.items.find(i => i.id === action.payload.id);
if (existingItem) {
existingItem.quantity += 1;
} else {
state.items.push({ ...action.payload, quantity: 1 });
}
state.total += action.payload.price;
},
removeItem: (state, action: PayloadAction<string>) => {
const index = state.items.findIndex(i => i.id === action.payload);
if (index !== -1) {
state.total -= state.items[index].price * state.items[index].quantity;
state.items.splice(index, 1);
}
},
clearCart: (state) => {
state.items = [];
state.total = 0;
}
}
});
export const { addItem, removeItem, clearCart } = cartSlice.actions;
// Store
const store = configureStore({
reducer: {
cart: cartSlice.reducer,
// другие slices
}
});
export type RootState = ReturnType<typeof store.getState>;
export type AppDispatch = typeof store.dispatch;
// Типизированные хуки
import { useDispatch, useSelector, TypedUseSelectorHook } from 'react-redux';
export const useAppDispatch = () => useDispatch<AppDispatch>();
export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector;
// Использование
function CartButton() {
const itemCount = useAppSelector((state) => state.cart.items.length);
return <button>Cart ({itemCount})</button>;
}
function AddToCartButton({ product }: { product: Product }) {
const dispatch = useAppDispatch();
return (
<button onClick={() => dispatch(addItem(product))}>
Add to Cart
</button>
);
}
Redux Async Actions (RTK Query)
import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react';
const api = createApi({
reducerPath: 'api',
baseQuery: fetchBaseQuery({ baseUrl: '/api' }),
tagTypes: ['Products', 'Cart'],
endpoints: (builder) => ({
getProducts: builder.query<Product[], void>({
query: () => 'products',
providesTags: ['Products']
}),
addToCart: builder.mutation<Cart, { productId: string }>({
query: (body) => ({
url: 'cart',
method: 'POST',
body
}),
invalidatesTags: ['Cart']
})
})
});
export const { useGetProductsQuery, useAddToCartMutation } = api;
// Использование
function ProductList() {
const { data: products, isLoading, error } = useGetProductsQuery();
const [addToCart] = useAddToCartMutation();
if (isLoading) return <Spinner />;
return (
<ul>
{products?.map(product => (
<li key={product.id}>
{product.name}
<button onClick={() => addToCart({ productId: product.id })}>
Add
</button>
</li>
))}
</ul>
);
}
Сравнение Zustand и Redux
| Критерий | Zustand | Redux Toolkit |
|---|---|---|
| Размер | ~1KB | ~10KB |
| Boilerplate | Минимум | Больше |
| DevTools | Через middleware | Встроено |
| Async | Просто в actions | RTK Query / Thunk |
| Экосистема | Растёт | Огромная |
| Для проекта | Малый/средний | Любой размер |
Заключение
Zustand — отличный выбор для небольших и средних проектов благодаря простоте. Redux Toolkit подходит для крупных приложений с командой, где важны DevTools и экосистема. Оба решения типобезопасны и хорошо работают с TypeScript.
Начните с Zustand для простоты. Переходите на Redux, когда нужны RTK Query, сложные middleware или большая команда.
