Урок 35 из 49 · Месяц 5. React и Zustand
Обзор менеджеров состояния в React
Содержание урока
- Что такое состояние и где оно живёт
- Локальное и глобальное состояние
- Проблема prop drilling
- Клиентское и серверное состояние
- Context API — встроенный в React
- Корзина на Context
- Плюсы и минусы Context
- Redux Toolkit — индустриальный стандарт
- Корзина на Redux Toolkit
- Когда Redux
- Zustand — просто и быстро
- MobX — реактивные объекты
- Jotai — состояние из маленьких атомов
- Сравнение и выбор
- Простой алгоритм выбора
- Типичные ошибки
- Практика
- Итоги
Когда приложение растёт, одни и те же данные — пользователь, корзина, тема оформления — нужны сразу многим компонентам. Передавать их пропсами через пять уровней неудобно, и тут на помощь приходят менеджеры состояния. В этом уроке разберём, какие они бывают (Context, Redux Toolkit, Zustand, MobX, Jotai), напишем одну и ту же корзину на каждом и поймём, что выбрать для своего проекта.
Что такое состояние и где оно живёт
Состояние (state) — это данные, которые меняются во время работы приложения и влияют на то, что видно на экране: текст в поле, открыт ли модал, список товаров в корзине.
Локальное и глобальное состояние
- Локальное — нужно одному компоненту: открыт ли выпадающий список, значение поля ввода. Для него хватает
useState(урок useState и useEffect). - Глобальное — нужно многим компонентам в разных частях дерева: авторизованный пользователь, корзина, язык интерфейса.
Проблема prop drilling
Представим: корзину хранит App, а количество товаров показывает значок в Header → Nav → CartIcon.
function App() {
const [cart, setCart] = useState([]);
return <Header cart={cart} />;
}
function Header({ cart }) {
return <Nav cart={cart} />; // Header'у корзина не нужна, он просто передаёт
}
function Nav({ cart }) {
return <CartIcon cart={cart} />; // Nav'у тоже не нужна
}
function CartIcon({ cart }) {
return <span>🛒 {cart.length}</span>;
}
Это называется prop drilling («сверление пропсами»): данные протаскиваются через компоненты, которым они не нужны. Аналогия: чтобы передать записку соседу через три квартиры, вы стучитесь в каждую дверь по пути.
Менеджер состояния — это общее «хранилище» вне дерева компонентов. Любой компонент может напрямую взять из него данные или изменить их, без посредников.
Клиентское и серверное состояние
Важно различать два вида данных:
| Клиентское состояние | Серверное состояние | |
|---|---|---|
| Что это | Живёт только в браузере | Копия данных с сервера |
| Примеры | Тема, открыт ли модал, корзина до оформления | Список товаров, профиль, заказы |
| Чем управлять | Context, Zustand, Redux, Jotai | TanStack Query (уроки React Query и дальше) |
Context API — встроенный в React
Context — механизм React, который позволяет «раздать» значение всем вложенным компонентам без пропсов. Ничего устанавливать не нужно.
Корзина на Context
// src/context/CartContext.jsx
import { createContext, useContext, useState } from "react";
const CartContext = createContext(null);
export function CartProvider({ children }) {
const [items, setItems] = useState([]);
const addItem = (product) => setItems((prev) => [...prev, product]);
const removeItem = (id) =>
setItems((prev) => prev.filter((item) => item.id !== id));
return (
<CartContext value={{ items, addItem, removeItem }}>
{children}
</CartContext>
);
}
export function useCart() {
const ctx = useContext(CartContext);
if (!ctx) throw new Error("useCart нужно использовать внутри CartProvider");
return ctx;
}
Разбор:
createContext(null)создаёт «канал» для передачи данных.CartProviderхранит состояние через обычныйuseStateи раздаёт его всем детям.<CartContext value={...}>— провайдер. В React 19 контекст можно использовать как провайдер напрямую; в React 18 и старше пишут<CartContext.Provider value={...}>.useCart— свой хук, чтобы не писатьuseContext(CartContext)в каждом компоненте и сразу ловить ошибку, если провайдер забыли.
Подключаем и используем:
// main.jsx
<CartProvider>
<App />
</CartProvider>
// CartIcon.jsx — в любом месте дерева
function CartIcon() {
const { items } = useCart();
return <span>🛒 {items.length}</span>;
}
Плюсы и минусы Context
- ✅ Встроен в React, прост для редко меняющихся данных: тема, язык, текущий пользователь.
- ❌ Когда значение меняется, перерисовываются все компоненты, которые используют этот контекст, даже если им нужна только часть данных.
- ❌ Много провайдеров превращаются в «пирамиду» из обёрток.
Redux Toolkit — индустриальный стандарт
Redux — самый известный менеджер состояния. Вся информация хранится в одном объекте — store (хранилище). Изменить его можно только отправив action (действие) — объект с описанием «что произошло». Reducer — функция, которая по action вычисляет новое состояние.
Redux Toolkit (RTK) — официальный современный способ писать на Redux: меньше кода и меньше ошибок.
npm install @reduxjs/toolkit react-redux
Корзина на Redux Toolkit
// src/store/cartSlice.js
import { createSlice } from "@reduxjs/toolkit";
const cartSlice = createSlice({
name: "cart",
initialState: { items: [] },
reducers: {
addItem(state, action) {
state.items.push(action.payload);
},
removeItem(state, action) {
state.items = state.items.filter((item) => item.id !== action.payload);
},
},
});
export const { addItem, removeItem } = cartSlice.actions;
export default cartSlice.reducer;
// src/store/index.js
import { configureStore } from "@reduxjs/toolkit";
import cartReducer from "./cartSlice";
export const store = configureStore({
reducer: { cart: cartReducer },
});
// main.jsx
import { Provider } from "react-redux";
import { store } from "./store";
<Provider store={store}>
<App />
</Provider>
// Компоненты
import { useDispatch, useSelector } from "react-redux";
import { addItem } from "./store/cartSlice";
function CartIcon() {
const count = useSelector((state) => state.cart.items.length);
return <span>🛒 {count}</span>;
}
function ProductCard({ product }) {
const dispatch = useDispatch();
return <button onClick={() => dispatch(addItem(product))}>В корзину</button>;
}
Разбор:
createSlice— создаёт «кусочек» хранилища: начальное состояние + функции изменения. Внутри можно «мутировать»state.items.push(...)— RTK под капотом (через библиотеку Immer) превратит это в безопасное создание нового объекта.cartSlice.actions— готовые функции-создатели action:addItem(product)вернёт{ type: "cart/addItem", payload: product }.useSelector— достаёт из store нужную часть. Компонент перерисуется, только если эта часть изменилась.useDispatch— даёт функциюdispatch, чтобы отправить action.
Когда Redux
- ✅ Большие команды и проекты: строгая структура, отличные DevTools (видно каждое действие и «перемотку времени»).
- ✅ Много вакансий его требуют — знать основы полезно.
- ❌ Больше шаблонного кода и понятий, чем в альтернативах.
Zustand — просто и быстро
Zustand (по-немецки «состояние») — маленькая библиотека: хранилище создаётся одной функцией, провайдер не нужен.
npm install zustand
// src/store/useCartStore.js
import { create } from "zustand";
export const useCartStore = create((set) => ({
items: [],
addItem: (product) => set((state) => ({ items: [...state.items, product] })),
removeItem: (id) =>
set((state) => ({ items: state.items.filter((item) => item.id !== id) })),
}));
function CartIcon() {
const count = useCartStore((state) => state.items.length);
return <span>🛒 {count}</span>;
}
Сравните с Redux: тот же результат в несколько раз меньшим кодом. Подробно разберём Zustand в следующем уроке — Знакомство с Zustand.
MobX — реактивные объекты
MobX работает по другому принципу: вы описываете обычный класс, а MobX «следит» за его полями. Изменили поле — все компоненты, которые его читают, перерисовались сами. Похоже на таблицу Excel: поменяли ячейку — пересчитались все формулы, которые от неё зависят.
npm install mobx mobx-react-lite
// src/store/cartStore.js
import { makeAutoObservable } from "mobx";
class CartStore {
items = [];
constructor() {
makeAutoObservable(this);
}
addItem(product) {
this.items.push(product);
}
get count() {
return this.items.length;
}
}
export const cartStore = new CartStore();
import { observer } from "mobx-react-lite";
import { cartStore } from "./store/cartStore";
const CartIcon = observer(() => <span>🛒 {cartStore.count}</span>);
makeAutoObservable(this)делает поля «наблюдаемыми», методы — действиями, а геттеры (get count) — вычисляемыми значениями.observerоборачивает компонент, чтобы он перерисовывался при изменении данных.
✅ Мало кода, привычный ООП-стиль. ❌ «Магия»: сложнее понять, почему компонент перерисовался; забыли observer — ничего не обновляется.
Jotai — состояние из маленьких атомов
Jotai (по-японски «состояние») хранит данные не в одном большом store, а в маленьких независимых кусочках — атомах. Работает почти как useState, только общий для всего приложения.
npm install jotai
import { atom, useAtom, useAtomValue } from "jotai";
const cartAtom = atom([]);
const cartCountAtom = atom((get) => get(cartAtom).length); // производный атом
function ProductCard({ product }) {
const [cart, setCart] = useAtom(cartAtom);
return <button onClick={() => setCart([...cart, product])}>В корзину</button>;
}
function CartIcon() {
const count = useAtomValue(cartCountAtom);
return <span>🛒 {count}</span>;
}
atom([])— создаёт атом с начальным значением.atom((get) => ...)— производный атом: вычисляется из других и пересчитывается сам.useAtomвозвращает[значение, сеттер], какuseState;useAtomValue— только значение.
Сравнение и выбор
| Инструмент | Установка | Объём кода | Провайдер | Когда выбрать |
|---|---|---|---|---|
| Context | Встроен | Средний | Нужен | Тема, язык, редкие изменения |
| Redux Toolkit | @reduxjs/toolkit react-redux |
Больше всех | Нужен | Крупный проект, большая команда |
| Zustand | zustand |
Минимальный | Не нужен | Большинство небольших и средних проектов |
| MobX | mobx mobx-react-lite |
Мало | Не обязателен | Любите классы и реактивность |
| Jotai | jotai |
Мало | Не обязателен | Много мелких независимых кусочков состояния |
Есть и другие: Valtio (похож на MobX, от авторов Zustand), Recoil (больше не развивается), Effector (популярен в русскоязычном сообществе). Принципы у всех похожи.
Простой алгоритм выбора
- Данные нужны одному компоненту? →
useState. - Это данные с сервера? → TanStack Query.
- Глобальные, редко меняются (тема, язык)? → Context.
- Глобальные и часто меняются? → Zustand (или Redux Toolkit, если так принято в команде).
Типичные ошибки
- Компонент вне провайдера. Для Context и Redux: вызвали
useCart()/useSelectorв компоненте, который не обёрнут провайдером, — ошибка илиnull. - Мутация состояния в Context/useState/Zustand.
items.push(x)без создания нового массива — React не заметит изменений. «Мутировать» можно только внутриcreateSlice(Immer) и в MobX. - Выбор всего store целиком.
useSelector((state) => state)перерисует компонент при любом изменении. Выбирайте только нужное. - Один огромный контекст на всё приложение — каждое изменение перерисовывает всё. Делите контексты по смыслу.
Практика
- Тема через Context. Сделайте
ThemeContextсо значением"light" | "dark"и кнопкой переключения в шапке. Ожидаемый результат: карточки в глубине дерева меняют фон без передачи пропсов. - Счётчик на трёх библиотеках. Реализуйте счётчик с кнопками «+», «−» и «Сброс» на Redux Toolkit, Zustand и Jotai. Сравните, сколько строк кода понадобилось в каждом случае.
- Корзина на Redux Toolkit. Выведите список из 5 товаров (массив в коде), кнопку «В корзину» у каждого и значок с количеством товаров в шапке. Добавьте action
clearCart. - Корзина на Zustand. Повторите задание 3 на Zustand и добавьте подсчёт общей суммы. Ожидаемый результат: одинаковое поведение, заметно меньше кода.
- Аргументированный выбор. Опишите в 5–7 предложениях, какой менеджер состояния вы бы выбрали для интернет-магазина, админки и блога, и почему.
Итоги
- Глобальное состояние нужно, когда данные используют многие компоненты; без него появляется prop drilling.
- Context встроен в React и подходит для редко меняющихся данных, но перерисовывает всех подписчиков.
- Redux Toolkit — строгий и популярный в крупных командах:
createSlice,configureStore,useSelector,useDispatch. - Zustand — минимум кода и без провайдера, хороший выбор по умолчанию.
- MobX делает объекты реактивными, Jotai строит состояние из атомов.
- Серверные данные лучше хранить не в менеджере состояния, а в TanStack Query.