Redux & State Management

Плюсы и минусы Redux

Плюсы:

  • Централизованное хранилище (single source of truth)
  • Предсказуемые мутации — только через dispatch(action)reducer
  • DevTools с time-travel отладкой (можно «перематывать» состояние)
  • Middleware (Redux Thunk, Redux Saga) для асинхронной логики

Минусы:

  • Много бойлерплейта: actions, action creators, reducers, constants
  • Избыточен для небольших приложений (хватит useState + Context)
  • Обновления фреймворка могут ломать обратную совместимость

Redux Toolkit (RTK) сильно сокращает бойлерплейт, но всё равно требует понимания концепций.
Альтернативы: Zustand, Jotai, MobX, React Query (для серверного состояния)


Для чего был придуман Redux?

Redux решает проблему непредсказуемого глобального состояния в больших приложениях. Основная идея — единый источник правды, состояние которого меняется только через явные действия.

Проблема без Redux:

Компонент A → пропс → Компонент B → пропс → Компонент C
// Prop drilling: нужно пробрасывать через все уровни
// Трудно отследить, кто и когда изменил данные

Три принципа Redux:

  1. Единый источник правды — весь state в одном Store.
  2. State только для чтения — изменить можно только через dispatch(action).
  3. Чистые редьюсеры — изменение state = чистая функция без побочных эффектов.

Из чего состоит Redux?

UI → dispatch(action) → Middleware → Reducer → Store → UI
// 1. ACTION — описывает что произошло
const increment = (amount) => ({
  type: 'counter/increment',
  payload: amount,
});
 
// 2. REDUCER — чистая функция: (state, action) => newState
const counterReducer = (state = 0, action) => {
  switch (action.type) {
    case 'counter/increment': return state + action.payload;
    case 'counter/decrement': return state - action.payload;
    default: return state;
  }
};
 
// 3. STORE — хранилище
import { createStore } from 'redux';
const store = createStore(counterReducer);
 
store.getState();          // 0
store.dispatch(increment(5)); // отправить действие
store.getState();          // 5
 
// 4. SELECTOR — функция для получения части state
const selectCount = (state) => state.counter;

Redux Toolkit (современный подход):

import { createSlice, configureStore } from '@reduxjs/toolkit';
 
const counterSlice = createSlice({
  name: 'counter',
  initialState: 0,
  reducers: {
    increment: (state, action) => state + action.payload,
    decrement: (state, action) => state - action.payload,
  },
});
 
const store = configureStore({ reducer: { counter: counterSlice.reducer } });
export const { increment, decrement } = counterSlice.actions;

Для чего используется Middleware?

Middleware — это функция между dispatch и reducer. Позволяет перехватывать actions и добавлять логику.

dispatch(action) → [middleware1] → [middleware2] → reducer
// Структура кастомного middleware
const myMiddleware = (store) => (next) => (action) => {
  console.log('Before:', store.getState());
  const result = next(action); // передать дальше
  console.log('After:', store.getState());
  return result;
};

Основные применения:

// 1. Асинхронные действия — Redux Thunk
const fetchUser = (id) => async (dispatch) => {
  dispatch({ type: 'user/loading' });
  try {
    const user = await api.getUser(id);
    dispatch({ type: 'user/loaded', payload: user });
  } catch (e) {
    dispatch({ type: 'user/error', payload: e.message });
  }
};
 
// В Redux Toolkit (createAsyncThunk)
const fetchUser = createAsyncThunk('user/fetch', async (id) => {
  const res = await api.getUser(id);
  return res.data;
});
 
// 2. Логирование — redux-logger
// 3. Обработка ошибок — централизованный try/catch
// 4. Аналитика — отслеживание actions
MiddlewareНазначение
redux-thunkАсинхронные actions (функции)
redux-sagaСложная async-логика через генераторы
redux-loggerЛогирование actions в console
redux-persistСохранение state в localStorage

Context API vs Redux

ПараметрContext APIRedux
СложностьМинимальнаяВысокая (экшены, редьюсеры)
ПроизводительностьРе-рендер всех потребителей контекстаТочечные подписки через селекторы
DevToolsНетRedux DevTools (time-travel)
Middleware / AsyncНетЕсть (thunk, saga)
Размер бандла0 КБ (встроен в React)заметный: RTK + React-Redux ~10+ КБ (min+gzip)
Когда использоватьТема, язык, авторизация — редко меняетсяСложный shared state, частые обновления

Правило выбора:

Редко меняется (тема, auth) → Context API
Часто обновляется, сложная логика → Redux Toolkit

В большинстве проектов комбинируют оба: Context для темы/локали, Redux для данных приложения.


В чём разница между Redux и MobX?

ReduxMobX
Модель измененийЯвные actionsредьюсеры (чистые функции), предсказуемый потокРеактивность (observable state), изменения часто через прямые присваивания / декораторы
СтруктураОдин store (или slice’ы), нормализованный подходОбычно множество observable-объектов, ООП-стиль
ПроизводительностьТонкая подписка через селекторы (reselect, RTK)Гранулярные реакции на изменение полей
ШаблонностьБольше бойлерплейта (без Toolkit); с RTK — меньшеМеньше шаблонного кода
Девтулзы / отладкаRedux DevTools, time-travelMobX DevTools
Когда уместноКомандные соглашения, строгий однонаправленный поток, аудит действийБыстрый старт, сложные графы зависимостей, «реактивный» UI

Оба решают задачу управления состоянием; Redux акцентирует явный поток событий, MobX — автоматическое отслеживание зависимостей.


Что такое Redux Thunk и Redux Saga?

Redux ThunkRedux Saga
Идеяdispatch может принимать функцию; thunk получает dispatch и getStateПобочные эффекты описываются генераторами и итераторами yield
Асинхронностьasync/await внутри thunkcall, put, take, fork и др. — декларативные эффекты
СложностьПроще войтиКруче кривая обучения
Отмена / гонкиВручную (флаги, AbortController)Встроенные паттерны (takeLatest, race, cancel)
Зависимостиredux-thunk (часто встроен в RTK)redux-saga

Thunk — middleware: если action — функция, middleware вызывает её и не передаёт функцию в редьюсер.

Saga — отдельные «процессы», слушающие actions и оркестрирующие асинхронные сценарии.


Как подключить Thunk / Saga к хранилищу?

Redux Thunk (классический createStore):

import { createStore, applyMiddleware } from 'redux';
import { thunk } from 'redux-thunk'; // v3+: именованный импорт (раньше был default)
import rootReducer from './reducers';
 
const store = createStore(rootReducer, applyMiddleware(thunk));

Redux Toolkit (Thunk уже в configureStore по умолчанию):

import { configureStore } from '@reduxjs/toolkit';
 
const store = configureStore({
  reducer: rootReducer,
  // thunk включён по умолчанию; можно добавить свой middleware:
  middleware: (getDefault) => getDefault().concat(myMiddleware),
});

Redux Saga:

import { createStore, applyMiddleware } from 'redux';
import createSagaMiddleware from 'redux-saga';
import rootReducer from './reducers';
import rootSaga from './sagas';
 
const saga = createSagaMiddleware();
const store = createStore(rootReducer, applyMiddleware(saga));
saga.run(rootSaga);

С RTK saga подключают через configureStore + middleware: (gD) => gD({ thunk: false }).concat(saga) или оставляют thunk и добавляют saga рядом — зависит от проекта.

Для собеса: Thunk — функции в dispatch; Saga — sagaMiddleware.run(rootSaga) после создания store.