Настройка State Management (Redux) для React-приложения
Redux — предсказуемый контейнер состояния с однонаправленным потоком данных. Одно глобальное хранилище, чистые reducer-функции, явные action-объекты. Для приложений с сложной бизнес-логикой, разделённой между многими компонентами, Redux даёт полный контроль над тем, как и когда меняется состояние.
Мы настраиваем Redux с современным стеком: Redux Toolkit для устранения boilerplate, RTK Query для серверного состояния, Redux DevTools для отладки.
Когда Redux оправдан
Redux нужен не для каждого проекта. Признаки того, что он уместен:
- Состояние разделяется между 5+ несвязанными компонентами
- Сложные переходы между состояниями с бизнес-правилами
- Нужна полная история изменений (time-travel debugging)
- Несколько источников данных обновляют одно состояние
- Команда 5+ разработчиков, нужна предсказуемость
Для локального состояния компонента — useState. Для серверных данных — TanStack Query или RTK Query. Redux — только для глобального клиентского состояния.
Структура хранилища
src/
store/
index.ts # Конфигурация store
hooks.ts # Типизированные useAppDispatch, useAppSelector
slices/
authSlice.ts
cartSlice.ts
uiSlice.ts
api/
productsApi.ts # RTK Query endpoints
// store/index.ts
import { configureStore } from '@reduxjs/toolkit';
import { authSlice } from './slices/authSlice';
import { cartSlice } from './slices/cartSlice';
import { uiSlice } from './slices/uiSlice';
import { productsApi } from './api/productsApi';
export const store = configureStore({
reducer: {
auth: authSlice.reducer,
cart: cartSlice.reducer,
ui: uiSlice.reducer,
[productsApi.reducerPath]: productsApi.reducer,
},
middleware: (getDefault) =>
getDefault().concat(productsApi.middleware),
});
export type RootState = ReturnType<typeof store.getState>;
export type AppDispatch = typeof store.dispatch;
// store/hooks.ts — типизированные хуки
import { useDispatch, useSelector } from 'react-redux';
import type { RootState, AppDispatch } from './index';
export const useAppDispatch = useDispatch.withTypes<AppDispatch>();
export const useAppSelector = useSelector.withTypes<RootState>();
Slice с бизнес-логикой
// store/slices/cartSlice.ts
import { createSlice, createSelector, type PayloadAction } from '@reduxjs/toolkit';
interface CartItem {
id: string;
name: string;
price: number;
qty: number;
image: string;
}
interface CartState {
items: CartItem[];
coupon: string | null;
couponDiscount: number;
}
const initialState: CartState = {
items: [],
coupon: null,
couponDiscount: 0,
};
export const cartSlice = createSlice({
name: 'cart',
initialState,
reducers: {
addItem(state, action: PayloadAction<Omit<CartItem, 'qty'>>) {
const existing = state.items.find(i => i.id === action.payload.id);
if (existing) {
existing.qty += 1;
} else {
state.items.push({ ...action.payload, qty: 1 });
}
},
removeItem(state, action: PayloadAction<string>) {
state.items = state.items.filter(i => i.id !== action.payload);
},
updateQty(state, action: PayloadAction<{ id: string; qty: number }>) {
const item = state.items.find(i => i.id === action.payload.id);
if (item) {
item.qty = Math.max(1, action.payload.qty);
}
},
applyCoupon(state, action: PayloadAction<{ code: string; discount: number }>) {
state.coupon = action.payload.code;
state.couponDiscount = action.payload.discount;
},
clearCart(state) {
state.items = [];
state.coupon = null;
state.couponDiscount = 0;
},
},
});
// Мемоизированные селекторы
export const selectCartItems = (state: RootState) => state.cart.items;
export const selectCartTotal = createSelector(
selectCartItems,
(state: RootState) => state.cart.couponDiscount,
(items, discount) => {
const subtotal = items.reduce((sum, item) => sum + item.price * item.qty, 0);
return subtotal * (1 - discount / 100);
}
);
export const selectCartCount = createSelector(
selectCartItems,
items => items.reduce((sum, item) => sum + item.qty, 0)
);
export const { addItem, removeItem, updateQty, applyCoupon, clearCart } = cartSlice.actions;
RTK Query для серверного состояния
// store/api/productsApi.ts
import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react';
export const productsApi = createApi({
reducerPath: 'productsApi',
baseQuery: fetchBaseQuery({
baseUrl: '/api',
prepareHeaders: (headers, { getState }) => {
const token = (getState() as RootState).auth.token;
if (token) headers.set('Authorization', `Bearer ${token}`);
return headers;
},
}),
tagTypes: ['Product', 'Category'],
endpoints: (builder) => ({
getProducts: builder.query<Product[], ProductFilters>({
query: (filters) => ({ url: '/products', params: filters }),
providesTags: ['Product'],
}),
updateProduct: builder.mutation<Product, Partial<Product> & { id: string }>({
query: ({ id, ...body }) => ({ url: `/products/${id}`, method: 'PUT', body }),
invalidatesTags: ['Product'],
}),
}),
});
export const { useGetProductsQuery, useUpdateProductMutation } = productsApi;
Использование в компонентах
import { useAppDispatch, useAppSelector } from '@/store/hooks';
import { addItem, selectCartCount } from '@/store/slices/cartSlice';
import { useGetProductsQuery } from '@/store/api/productsApi';
function ProductCard({ productId }: { productId: string }) {
const dispatch = useAppDispatch();
const cartCount = useAppSelector(selectCartCount);
const { data: product, isLoading } = useGetProductsQuery({ id: productId });
if (isLoading) return <Skeleton />;
return (
<div>
<h3>{product.name}</h3>
<button onClick={() => dispatch(addItem(product))}>
В корзину ({cartCount})
</button>
</div>
);
}
Redux DevTools и отладка
Redux DevTools Extension позволяет:
- Просматривать историю всех actions
- Переходить к любому предыдущему состоянию (time-travel)
- Экспортировать/импортировать состояние для воспроизведения багов
// store/index.ts — дополнительная конфигурация DevTools
configureStore({
...
devTools: process.env.NODE_ENV !== 'production' && {
name: 'MyApp',
trace: true, // Трассировка вызовов action
traceLimit: 25,
},
});
Сроки реализации
- Неделя 1: настройка store, slices для основных доменов, типизированные хуки
- Неделя 2: RTK Query endpoints, интеграция с существующими компонентами
- Неделя 3: мемоизированные селекторы, оптимизация ре-рендеров (React.memo + reselect), тесты reducer-функций
- Неделя 4: документация архитектуры состояния, code review, настройка Redux DevTools в dev-окружении
Reducer-функции тестируются изолированно без React — это одно из главных преимуществ Redux-архитектуры перед хуками.







