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.

Связанные темы

Ресурсы