Сравнение пяти подходов к клиентскому состоянию в React с чёткими рекомендациями: Zustand как универсальный выбор для большинства новых проектов, Redux Toolkit для крупных команд с требованиями к аудиту, Context для редко меняющихся глобальных настроек, а Jotai и MobX для узких ниш — сложных форм и объектно-ориентированной модели. Главное условие — предварительно отделить серверные данные и отдать их TanStack Query, SWR или RTK Query, чтобы не дублировать кэш и не провоцировать лишние перерисовки.
Контекст
Выбор библиотеки — второе решение. Первое — разделить данные на серверные и клиентские.
| Задача | Решение |
| Данные приходят с API | TanStack 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, а доменная модель хорошо ложится в классы.
Сравнительная таблица
| Критерий | Context | Zustand | Redux Toolkit | Jotai | MobX |
| Модель | Провайдер + хук | Единый стор на хуках | Стор + слайсы | Атомы | Наблюдаемые объекты |
| Размер, gzip | 0 КБ | 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 |
| Проект с легаси-кодом на MobX | MobX, без миграции |
Чего избегать:
- Хранить серверные данные в глобальном сторе и руками чистить кэш.
- Один контекст на всё приложение.
- Тянуть Redux ради модального окна и темы.
- Смешивать два стора без договорённости, кто за что отвечает.
- Оптимизировать перерисовки до того, как появились замеры.
Выбор библиотеки обратим: Context остаётся для локальных задач, Zustand переезжает в RTK без переписывания логики. Дороже всего обходится не выбор, а отсутствие правил — кто владеет данными и где они живут.