Управление состоянием в React: сравнение Context, Zustand, Redux Toolkit, Jotai и MobX

Сравнение пяти подходов к клиентскому состоянию в React с чёткими рекомендациями: Zustand как универсальный выбор для большинства новых проектов, Redux Toolkit для крупных команд с требованиями к аудиту, Context для редко меняющихся глобальных настроек, а Jotai и MobX для узких ниш — сложных форм и объектно-ориентированной модели. Главное условие — предварительно отделить серверные данные и отдать их TanStack Query, SWR или RTK Query, чтобы не дублировать кэш и не провоцировать лишние перерисовки.

Сравнение пяти подходов к клиентскому состоянию в React с чёткими рекомендациями: Zustand как универсальный выбор для большинства новых проектов, Redux Toolkit для крупных команд с требованиями к аудиту, Context для редко меняющихся глобальных настроек, а Jotai и MobX для узких ниш — сложных форм и объектно-ориентированной модели. Главное условие — предварительно отделить серверные данные и отдать их TanStack Query, SWR или RTK Query, чтобы не дублировать кэш и не провоцировать лишние перерисовки.

Контекст

Выбор библиотеки — второе решение. Первое — разделить данные на серверные и клиентские.

ЗадачаРешение
Данные приходят с APITanStack Query, SWR или RTK Query
Тема, локаль, текущий пользовательContext
Общий UI-стейт в обычном проектеZustand
Крупная команда, долгий проект, аудит измененийRedux Toolkit
Сложные формы, редакторы, много зависимых значенийJotai
Команда мыслит классами и объектамиMobX
  • Дефолт 2026 года — TanStack Query для серверных данных плюс Zustand для клиентских.
  • Context не стор: у него нет селекторов, и он перерисовывает всех потребителей.
  • Redux Toolkit остаётся выбором крупных команд: строгий шаблон, лучший DevTools, RTK Query.
  • Jotai и MobX закрывают узкие задачи — атомарную гранулярность и ООП-модель соответственно.

Как выбирать: две оси

Состояние в React делят на два вида.

Серверное — данные, которые принадлежат бэкенду: списки, карточки, профили. Их нужно кэшировать, инвалидировать и обновлять в фоне. Этим занимаются TanStack Query, SWR и RTK Query. Держать такие данные в глобальном сторе — значит писать руками кэш с гонками и устаревшими ответами.

Клиентское — то, что живёт только в браузере: открытые панели, черновики форм, выбранные фильтры, тема.

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

Обзор: пять подходов за минуту

  • Context — встроенный механизм React. Передаёт значение вниз по дереву без пропсов. Состояния не хранит и не оптимизирует.
  • Zustand — глобальный стор на хуках. Живёт вне React, подписки точечные, шаблонного кода почти нет.
  • Redux Toolkit — единый стор со слайсами, экшенами и строгими правилами. RTK Query закрывает серверные данные.
  • Jotai — состояние разбито на атомы. Компонент перерисовывается только при изменении своих атомов.
  • MobX — классы с наблюдаемыми полями. Изменения отслеживаются через Proxy, диспатчи не нужны.

Context: встроенный механизм, а не менеджер состояния

Context решает одну задачу: отдать значение всем компонентам внутри провайдера. Само состояние держит useState или useReducer рядом.


const ThemeContext = createContext<Theme>('light');

function App() {

  const [theme, setTheme] = useState<Theme>('light');

  return (

    <ThemeContext.Provider value={theme}>

      <Header onToggle={() => setTheme((t) => (t === 'light' ? 'dark' : 'light'))} />

      <Main />

    </ThemeContext.Provider>

  );

}

function Button() {

  const theme = useContext(ThemeContext);

  return <button className={theme}>Купить</button>;

}

В React 19 провайдер пишется короче: <ThemeContext value={theme}>.

Главная ловушка — перерисовки. Любое изменение value обновляет всех потребителей. Если положить в один контекст объект { user, cart, settings }, правка корзины перерисует и компоненты, которым нужен только пользователь.

Что помогает:

  • разбить контекст на несколько независимых;
  • мемоизировать значение через useMemo;
  • вынести часто меняющуюся часть в стор.

Плюсы

  • Ноль зависимостей, встроен в React.
  • Понятен новичку.

Минусы

  • Нет селекторов и DevTools.
  • Перерисовывает всех потребителей.
  • Асинхронность и кэш пишутся руками.

Когда брать: тема, локаль, авторизация, фича-флаги — данные, которые меняются редко и нужны многим компонентам.

Zustand: минимальный шаблон и точечные подписки

Стор создаётся один раз и живёт вне React. Компоненты подписываются на нужный кусок через селектор и перерисовываются только при его изменении.


import { create } from 'zustand';

type CartItem = { id: string; title: string; price: number };

type CartStore = {

  items: CartItem[];

  add: (item: CartItem) => void;

  remove: (id: string) => void;

  total: () => number;

};

export const useCart = create<CartStore>((set, get) => ({

  items: [],

  add: (item) => set((state) => ({ items: [...state.items, item] })),

  remove: (id) =>

    set((state) => ({ items: state.items.filter((i) => i.id !== id) })),

  total: () => get().items.reduce((sum, i) => sum + i.price, 0),

}));

Подписка на длину корзины — компонент не узнает о правках цен и названий:


function CartBadge() {

  const count = useCart((state) => state.items.length);

  return <span>{count}</span>;

}

Асинхронный экшен — обычная функция, никаких thunk-обёрток:


export const useUser = create<UserStore>((set) => ({

  user: null,

  loading: false,

  load: async (id) => {

    set({ loading: true });

    const user = await api.getUser(id);

    set({ user, loading: false });

  },

}));

persist сохраняет стор в localStorage, immer разрешает мутации, devtools подключает отладку. Всё это middleware в одну строку.

Плюсы

  • 0,5 КБ и почти ноль шаблонного кода.
  • Стор доступен вне React: его можно читать из утилит и тестов.
  • Сторы дробятся по модулям, а не собираются в один комбайн.

Минусы

  • Правил нет: структуру и договорённости команда придумывает сама.
  • Сложные сценарии — отмена запросов, зависимости между сторами — проектируются руками.

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

Redux Toolkit: дисциплина за дополнительный код

Redux Toolkit убрал из классического Redux большую часть шаблонного кода. Слайс объединяет экшены и редьюсер в одном файле, Immer разрешает писать «мутации».


// features/counter/counterSlice.ts

const counterSlice = createSlice({

  name: 'counter',

  initialState: { value: 0 },

  reducers: {

    increment: (state) => {

      state.value += 1;

    },

    addBy: (state, action: PayloadAction<number>) => {

      state.value += action.payload;

    },

  },

});

export const { increment, addBy } = counterSlice.actions;

export const selectValue = (state: RootState) => state.counter.value;

// app/store.ts

export const store = configureStore({

  reducer: { counter: counterSlice.reducer },

});

export type RootState = ReturnType<typeof store.getState>;

function Counter() {

  const value = useSelector(selectValue);

  const dispatch = useDispatch();

  return <button onClick={() => dispatch(increment())}>{value}</button>;

}

Серверные данные берёт на себя RTK Query: кэш, повторные запросы, инвалидация и состояния загрузки появляются без ручного кода.


export const postsApi = createApi({

  reducerPath: 'posts',

  baseQuery: fetchBaseQuery({ baseUrl: '/api' }),

  endpoints: (build) => ({

    getPosts: build.query<Post[], void>({ query: () => 'posts' }),

  }),

});

export const { useGetPostsQuery } = postsApi;

Плюсы

  • Единый шаблон на всю команду: новый разработчик читает чужой код без сюрпризов.
  • Лучший DevTools в классе: тайм-тревел, лог экшенов, диффы состояния.
  • RTK Query закрывает серверное состояние целиком.
  • Огромная экосистема и готовые ответы на типовые вопросы.

Минусы

  • 14,6 КБ против 0,5 КБ у Zustand.
  • На каждое изменение — экшен, редьюсер и селектор.
  • Порог входа выше, чем у конкурентов.

Когда брать: крупная команда, долгий проект, требования к предсказуемости и аудиту изменений.

Jotai: состояние из мелких независимых кусков

Атом — минимальная единица состояния. Компонент подписывается на нужные атомы, а производные значения собираются из зависимостей без мемоизации.


const countAtom = atom(0);

const doubledAtom = atom((get) => get(countAtom) * 2);

function Counter() {

  const [count, setCount] = useAtom(countAtom);

  const doubled = useAtomValue(doubledAtom);

  return (

    <button onClick={() => setCount((c) => c + 1)}>

      {count} / {doubled}

    </button>

  );

}

Асинхронный атом — это просто async-функция внутри:


const userAtom = atom(async () => {

  const res = await fetch('/api/me');

  return res.json();

});

Для коллекций есть atomFamily: один атом на строку таблицы или ячейку формы. Для персистенции — atomWithStorage. В Jotai 3 atomFamily вынесен в отдельный пакет, а сам пакет стал ESM-only.

Плюсы

  • Самые точечные перерисовки из всего списка.
  • Производные значения считаются сами, без useMemo и селекторов.
  • Атом можно объявить рядом с компонентом, который его использует.

Минусы

  • Мышление «снизу вверх» поначалу непривычно.
  • Общая картина состояния размазана по файлам.
  • DevTools слабее, чем у Redux.

Когда брать: сложные формы, редакторы, таблицы, графы зависимостей — всё, где много мелкой производной логики.

MobX: наблюдаемые объекты вместо экшенов

MobX строит состояние на классах. Поля помечаются наблюдаемыми, геттеры становятся вычисляемыми значениями, а компонент-observer сам подписывается на то, что прочитал.


class TodoStore {

  todos: Todo[] = [];

  filter: 'all' | 'done' = 'all';

  constructor() {

    makeAutoObservable(this);

  }

  get visible() {

    return this.filter === 'done'

      ? this.todos.filter((t) => t.done)

      : this.todos;

  }

  add(title: string) {

    this.todos.push({ id: crypto.randomUUID(), title, done: false });

  }

}

const TodoList = observer(({ store }: { store: TodoStore }) => (

  <ul>

    {store.visible.map((t) => (

      <li key={t.id}>{t.title}</li>

    ))}

  </ul>

));

Перерисовка происходит только у тех компонентов, которые читали изменившееся поле. Вычисляемые геттеры кэшируются автоматически.

MobX 7 (июль 2026) — только Proxy, без легаси-декораторов и ES5-фолбэка. Пакет стал примерно на 40% меньше.

Плюсы

  • Минимум церемоний при высокой гранулярности.
  • ООП-модель близка разработчикам из Angular и с бэкенда.
  • Вычисляемые значения кэшируются сами.

Минусы

  • 15,1 КБ — самый тяжёлый из пяти.
  • observer легко забыть, и компонент перестанет обновляться.
  • Отладка «магии» Proxy сложнее явного потока экшенов.

Когда брать: команда уже знает MobX, а доменная модель хорошо ложится в классы.

Сравнительная таблица

КритерийContextZustandRedux ToolkitJotaiMobX
МодельПровайдер + хукЕдиный стор на хукахСтор + слайсыАтомыНаблюдаемые объекты
Размер, gzip0 КБ0,5 КБ14,6 КБ3,3 КБ15,1 КБ
ПерерисовкиВсе потребителиПо селекторуПо селекторуПо атомуПо наблюдаемым полям
DevToolsНетЕстьЛучшие в классеЕсть, базовыеЕсть
АсинхронностьВручнуюОбычные функцииThunk и RTK QueryАсинхронные атомыЭкшены и реакции
Кривая входаНизкаяОчень низкаяСредняяНизкаяСредняя
Когда братьРедкие глобальные настройкиМалые и средние проектыКрупные командыСложный UIООП-команды

Размеры — gzip по опубликованным пакетам на ноябрь 2026 года: Zustand 5, Redux Toolkit 2, Jotai 3, MobX 7. После tree shaking в вашей сборке цифры будут меньше.

Выводы и рекомендации

1. Сначала разделите данные. Серверное состояние — в TanStack Query или RTK Query. Клиентское — в стор или в Context. Большая часть «проблем со стейт-менеджментом» — это попытка держать серверные данные в глобальном сторе.

2. Для нового проекта берите Zustand. Девяносто процентов задач он закрывает без архитектурных решений. Если проект вырастет, переезд на Redux Toolkit почти механический: экшены и редьюсеры ложатся на те же операции.

3. Redux Toolkit — для крупной команды. Когда в проекте восемь разработчиков и пятьдесят экранов, строгий шаблон экономит больше времени, чем стоит. RTK Query заодно снимает вопрос с кэшем API.

4. Jotai — когда состояние плохо ложится в один объект. Редактор с независимыми блоками, форма на сотню полей, таблица с ячейками. Один общий стор здесь превратится в свалку.

5. MobX — если команда его уже знает. Переучивать команду ради MobX смысла нет, но и переписывать рабочий проект на Zustand только из-за моды не стоит.

6. Context не выбрасывайте. Для темы, локали и авторизации он остаётся лучшим вариантом: ноль зависимостей и никакой настройки.

Типовые сценарии:

СценарийЧто взять
Лендинг с формой заявкиContext + `useState`
SaaS-дашборд на 10–20 экрановTanStack Query + Zustand
Админка на 50 экранов и 8 разработчиковRedux Toolkit + RTK Query
Редактор документов, конструкторJotai
Проект с легаси-кодом на MobXMobX, без миграции

Чего избегать:

  • Хранить серверные данные в глобальном сторе и руками чистить кэш.
  • Один контекст на всё приложение.
  • Тянуть Redux ради модального окна и темы.
  • Смешивать два стора без договорённости, кто за что отвечает.
  • Оптимизировать перерисовки до того, как появились замеры.

Выбор библиотеки обратим: Context остаётся для локальных задач, Zustand переезжает в RTK без переписывания логики. Дороже всего обходится не выбор, а отсутствие правил — кто владеет данными и где они живут.

Ответить