Зачем нужен менеджер состояния в веб‑приложении
Зачем нужен менеджер состояния (Redux, MobX, Effector, Zustand) в веб-приложении?
Теория: что такое состояние и почему возникают проблемы
Состояние (state) — это данные, которые меняются со временем и влияют на то, что видит пользователь: авторизация, профиль, корзина, фильтры, результаты запросов, настройки интерфейса.
Типичная эволюция сложности
- Сначала состояние хранится локально в компонентах.
- Затем одни и те же данные становятся нужны нескольким веткам интерфейса.
- Начинается передача данных “сверху вниз” через несколько уровней компонентов (prop drilling).
- Появляется риск рассинхронизации: разные части интерфейса обновляют похожие данные по-разному и в разное время.
Что даёт менеджер состояния
- Единый источник истины для общих данных (часто это единое хранилище, “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покрывают потребности без сложных потоков данных.
Таблица: краткое сравнение подходов
| Библиотека | Основная идея | Как меняется состояние | Типичный плюс |
|---|---|---|---|
| Redux | Единое хранилище + явные действия и правила | dispatch(action) → reducer возвращает новый state | Предсказуемость и единый стиль изменений |
| MobX | Реактивные данные и вычисления | Изменение наблюдаемых данных, UI реагирует автоматически | Меньше “связующего” кода вокруг derived-логики |
| Effector | События и реактивные stores | Store обновляется через события и реакции | Явные потоки событий и хорошая композиция |
| Zustand | Минимальное хранилище | set(...), подписка через хук с селектором | Простота и малое количество абстракций |
Итого: менеджер состояния нужен для согласованного хранения и обновления общих данных в приложении: он снижает риск рассинхронизации, упрощает отслеживание изменений и уменьшает необходимость передачи данных через длинные цепочки компонентов.