Redux: концепции и архитектура
Redux — предсказуемый контейнер состояния для JavaScript-приложений, реализующий однонаправленный поток данных через три принципа: единственный источник истины, state доступен только для чтения, изменения через чистые функции.
Зачем нужно
Redux решает проблему управления сложным глобальным состоянием: данные корзины, авторизованный пользователь, фильтры поиска — всё в одном месте, изменения предсказуемы и воспроизводимы. Redux DevTools позволяет «путешествовать во времени» — откатывать и воспроизводить actions. Redux Toolkit (RTK) — современный способ писать Redux без бойлерплейта.
Где используется
- Крупные React-приложения с комплексным глобальным state
- Приложения с требованием к отлаживаемости (DevTools, time-travel debugging)
- Проекты с командой, знающей Redux экосистему
- Используется в связке с RTK Query для server state
Три принципа Redux
1. Single source of truth — одно дерево state для всего приложения
2. State is read-only — изменить state можно только через dispatch(action)
3. Changes via pure functions — reducer(state, action) → newState
Redux Toolkit (современный Redux)
// store/cartSlice.js — RTK slice заменяет actions + reducer
import { createSlice } from '@reduxjs/toolkit';
const cartSlice = createSlice({
name: 'cart',
initialState: {
items: , // { id, name, price, quantity }
isOpen: false,
},
reducers: {
// RTK использует Immer — можно "мутировать" внутри reducer
addItem(state, action) {
const existing = state.items.find(i => i.id === action.payload.id);
if (existing) {
existing.quantity += 1;
} else {
state.items.push({ ...action.payload, quantity: 1 });
}
},
removeItem(state, action) {
state.items = state.items.filter(i => i.id !== action.payload);
},
clearCart(state) {
state.items = ;
},
toggleCart(state) {
state.isOpen = !state.isOpen;
},
},
});
// Экспорт action creators и reducer
export const { addItem, removeItem, clearCart, toggleCart } = cartSlice.actions;
export default cartSlice.reducer;
// Selectors — вычисляемые значения из state
export const selectCartItems = (state) => state.cart.items;
export const selectCartTotal = (state) =>
state.cart.items.reduce((sum, item) => sum + item.price * item.quantity, 0);
export const selectCartCount = (state) =>
state.cart.items.reduce((sum, item) => sum + item.quantity, 0);
// store/index.js — конфигурация store
import { configureStore } from '@reduxjs/toolkit';
import cartReducer from './cartSlice';
import authReducer from './authSlice';
export const store = configureStore({
reducer: {
cart: cartReducer,
auth: authReducer,
},
// DevTools подключаются автоматически в development
});
// Использование в компонентах
import { useSelector, useDispatch } from 'react-redux';
import { addItem, selectCartTotal, selectCartCount } from './store/cartSlice';
function CartButton() {
const count = useSelector(selectCartCount);
const total = useSelector(selectCartTotal);
return (
<button>
Корзина ({count}) — {total} ₽
</button>
);
}
function ProductCard({ product }) {
const dispatch = useDispatch;
return (
<div>
<h3>{product.name}</h3>
<button onClick={ => dispatch(addItem(product))}>
В корзину
</button>
</div>
);
}
// Provider в корне
function App() {
return (
<Provider store={store}>
<Router>...</Router>
</Provider>
);
}
Async Actions с createAsyncThunk
// Асинхронные операции
import { createAsyncThunk } from '@reduxjs/toolkit';
export const fetchProducts = createAsyncThunk(
'products/fetchAll',
async (filters, { rejectWithValue }) => {
try {
const response = await api.get('/products', { params: filters });
return response.data;
} catch (error) {
return rejectWithValue(error.response.data);
}
}
);
// В slice:
extraReducers: (builder) => {
builder
.addCase(fetchProducts.pending, (state) => {
state.isLoading = true;
})
.addCase(fetchProducts.fulfilled, (state, action) => {
state.isLoading = false;
state.items = action.payload;
})
.addCase(fetchProducts.rejected, (state, action) => {
state.isLoading = false;
state.error = action.payload;
});
},
Частые ошибки
- Redux для локального состояния —
isDropdownOpenв Redux — антипаттерн; оставляйте UI-состояние вuseState. - Мутация state без RTK — без Immer reducer должен возвращать новый объект через spread.
- Selector без memoization при тяжёлых вычислениях — используйте
createSelectorизreselectдля производительных селекторов. - Слишком большой slice — разбивайте по доменным областям:
cartSlice,authSlice,productsSlice.
Связанные темы
- _MOC SPA
- Flux Pattern
- Локальное vs глобальное состояние
- Context API -- обзор
- State -- внутреннее состояние