Зачем нужен менеджер состояния в веб‑приложении

Зачем нужен менеджер состояния (Redux, MobX, Effector, Zustand) в веб-приложении?

Теория: что такое состояние и почему возникают проблемы

Состояние (state) — это данные, которые меняются со временем и влияют на то, что видит пользователь: авторизация, профиль, корзина, фильтры, результаты запросов, настройки интерфейса.

Типичная эволюция сложности

  1. Сначала состояние хранится локально в компонентах.
  2. Затем одни и те же данные становятся нужны нескольким веткам интерфейса.
  3. Начинается передача данных “сверху вниз” через несколько уровней компонентов (prop drilling).
  4. Появляется риск рассинхронизации: разные части интерфейса обновляют похожие данные по-разному и в разное время.
Менеджер состояния не является обязательной частью каждого проекта: в небольших приложениях нередко достаточно локального состояния компонентов и/или React Context, а отдельная библиотека чаще нужна при росте масштаба и сложности.

Что даёт менеджер состояния

  • Единый источник истины для общих данных (часто это единое хранилище, “store”).
  • Явные способы изменения состояния: через события/действия и правила обновления (например, редьюсеры).
  • Реактивные подписки: при изменении данных связанные части интерфейса обновляются автоматически.
  • Упрощение диагностики: становится проще понять, что произошло (событие/действие) и по какому правилу изменились данные.
Добавление менеджера состояния увеличивает “архитектурную нагрузку”: требуется договориться о структуре состояния и о том, как именно оно меняется; в маленьком проекте это может замедлять разработку.

Пример проблемы: prop drilling и рассинхронизация

Ниже показана упрощённая ситуация: значение user требуется и вверху, и глубоко внутри дерева, а также участвует в загрузке данных.

// Пример: данные прокидываются через несколько уровней,
// хотя промежуточным компонентам они не нужны.

function App() {
  const [user, setUser] = React.useState(null);

  React.useEffect(() => {
    fetch("/api/me")
      .then((r) => r.json())
      .then(setUser);
  }, []);

  return (
    <Layout user={user}>
      <Page user={user} />
    </Layout>
  );
}

function Layout({ user, children }) {
  return (
    <div>
      <Header user={user} />
      {children}
    </div>
  );
}

function Page({ user }) {
  return <ProfileCard user={user} />;
}

function ProfileCard({ user }) {
  if (!user) return <span>Loading...</span>;
  return <div>Hello, {user.name}</div>;
}

Проблемы такого подхода в больших приложениях:

  • Сложно менять структуру дерева компонентов: приходится “протягивать” пропсы через новые уровни.
  • Трудно гарантировать единообразное обновление, если похожие данные начинают храниться в нескольких местах.
  • Сложно отлаживать, кто и когда изменил данные, если обновления распределены по множеству компонентов.

Схематично:

Без менеджера:
App (state)
  ├─ Layout (просто пересылает user)
  │    └─ Header (читает user)
  └─ Page (просто пересылает user)
       └─ ProfileCard (читает user)

С менеджером:
Global Store (state)
  ├─ Header (подписка на store)
  └─ ProfileCard (подписка на store)

Как это делают разные менеджеры состояния

Ниже приведены минимальные примеры, чтобы увидеть общую идею: состояние выносится из компонентов, а компоненты подписываются на изменения.

Redux: store, action, reducer

Идея: состояние хранится в store, изменение инициируется через dispatch(action), а новый state вычисляется функцией reducer.

import { createStore } from "redux";

// Action (описание “что произошло”)
const setUser = (user) => ({ type: "user/set", payload: user });

// Reducer (правило “как меняется state”)
function reducer(state = { user: null }, action) {
  switch (action.type) {
    case "user/set":
      return { ...state, user: action.payload };
    default:
      return state;
  }
}

export const store = createStore(reducer);

// Где-то в коде загрузки данных:
fetch("/api/me")
  .then((r) => r.json())
  .then((user) => store.dispatch(setUser(user)));

// В UI (псевдокод): подписка на store и чтение store.getState()

Ключевая мысль: компонентам не требуется получать user через цепочку пропсов, потому что чтение идёт из общего хранилища.

MobX: реактивные значения и производные данные

Идея: есть “наблюдаемые” данные, а UI автоматически обновляется, если были использованы значения, которые изменились.

import { makeAutoObservable } from "mobx";
import { observer } from "mobx-react-lite";

class UserStore {
  user = null;

  constructor() {
    makeAutoObservable(this);
  }

  setUser(user) {
    this.user = user;
  }

  get userName() {
    return this.user ? this.user.name : "Loading...";
  }
}

export const userStore = new UserStore();

// UI: компонент реагирует на изменения используемых полей
export const Header = observer(() => {
  return <div>Hello, {userStore.userName}</div>;
});

Ключевая мысль: производные значения (например, userName) удобно держать рядом с моделью и получать автоматически обновляемый UI.

Effector: stores + events, обновление через события

Идея: изменения описываются как события, а store обновляется реактивно в ответ на эти события.

import { createEvent, createStore } from "effector";

// Событие: “что произошло”
const userLoaded = createEvent();

// Store: “где хранятся данные”
const $user = createStore(null)
  .on(userLoaded, (_, user) => user);

// Где-то в коде загрузки:
fetch("/api/me")
  .then((r) => r.json())
  .then(userLoaded);

// UI (псевдокод): подписка на $user через bindings для фреймворка

Ключевая мысль: состояние меняется не “напрямую”, а через поток событий и описанные правила реакции store.

Zustand: минимальное хранилище и хук-подписка

Идея: создаётся небольшое хранилище, чтение делается через хук с селектором, обновление — через функцию set.

import { create } from "zustand";

const useUserStore = create((set) => ({
  user: null,
  setUser: (user) => set({ user }),
}));

function Header() {
  const user = useUserStore((s) => s.user);
  return <div>Hello, {user ? user.name : "Loading..."}</div>;
}

// Загрузка данных:
fetch("/api/me")
  .then((r) => r.json())
  .then((user) => useUserStore.getState().setUser(user));

Ключевая мысль: компонент получает данные напрямую из хранилища и обновляется, когда меняется выбранный “срез” состояния.

Когда менеджер состояния действительно нужен

Менеджер состояния обычно оправдан, когда:

  • Одни и те же данные используются во многих независимых компонентах (например, пользователь, права, корзина).
  • Есть сложные сценарии обновлений (несколько источников событий, асинхронные запросы, зависимые вычисления).
  • Нужны единые правила, чтобы понимать “кто изменил состояние и почему” (события/действия + правила обновления).

Часто можно обойтись без отдельной библиотеки, когда:

  • Состояние локальное и используется внутри одного компонента или небольшой ветки.
  • Поднятия состояния на уровень выше достаточно, и цепочки передачи не становятся длинными.
  • React Context + useReducer покрывают потребности без сложных потоков данных.
Выбор конкретной библиотеки обычно определяется архитектурой: строгие правила и предсказуемые обновления, реактивные вычисления, минималистичный API — это разные приоритеты, и они по-разному реализуются в Redux/MobX/Effector/Zustand.

Таблица: краткое сравнение подходов

БиблиотекаОсновная идеяКак меняется состояниеТипичный плюс
ReduxЕдиное хранилище + явные действия и правилаdispatch(action)reducer возвращает новый stateПредсказуемость и единый стиль изменений
MobXРеактивные данные и вычисленияИзменение наблюдаемых данных, UI реагирует автоматическиМеньше “связующего” кода вокруг derived-логики
EffectorСобытия и реактивные storesStore обновляется через события и реакцииЯвные потоки событий и хорошая композиция
ZustandМинимальное хранилищеset(...), подписка через хук с селекторомПростота и малое количество абстракций

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