Корзина нужна в шапке, на странице товара и при оформлении заказа. Передавать её через props на пять уровней неудобно, а context перерисовывает всех подписчиков на любое изменение. Тогда берут менеджер состояния: отдельное хранилище вне дерева компонентов, на которое компоненты подписываются.
Библиотек много, но устроены они по двум схемам. Redux хранит состояние неизменяемым: каждое действие создаёт новое состояние, а компонент перерисовывается, если изменилась нужная ему часть. MobX разрешает менять объекты напрямую и сам следит, кто какие поля читал. Ниже обе схемы изнутри, Redux Toolkit и RTK Query как современный Redux, и как выбрать. Когда стор вообще нужен, разобрано в статье про состояние.
Redux сравнивает ссылки после каждого действия, MobX заранее знает, кто читал изменённое поле.
Redux: новое состояние вместо изменённогоспросят на собеседовании
В Redux состояние меняют только через действие: объект вида { type: "cart/added", payload: "чайник" }. Действие попадает в редьюсер — функцию, которая по старому состоянию и действию возвращает новое. Компонент подписывается через useSelector и перерисовывается, если выбранная им часть стала другой.
«Другой» здесь значит другую ссылку: сравнение идёт через ===, а не по содержимому. Если редьюсер поменял массив на месте, ссылка осталась прежней, и компонент изменения не увидит. Поэтому новое состояние собирают копированием, а нетронутые ветки переиспользуют:
живой пример
const state = { cart: { items: ["чайник"] }, user: { name: "Аня" } };
function addMutating(s, item) {
s.cart.items.push(item);
return s;
}
function addImmutable(s, item) {
return { ...s, cart: { ...s.cart, items: [...s.cart.items, item] } };
}
const before = state.cart.items;
addMutating(state, "кружка");
console.log("мутация, ссылка та же:", state.cart.items === before);
const fresh = { cart: { items: ["чайник"] }, user: { name: "Аня" } };
const next = addImmutable(fresh, "кружка");
console.log("корзина получила новую ссылку:", next.cart.items !== fresh.cart.items);
console.log("пользователь остался прежним:", next.user === fresh.user);
Запустить
Запуск примеров доступен в платном доступе. Там этот же код выполняется прямо в статье: редактор, запуск и проверка рядом с абзацем. Три дня бесплатно →
Последняя строка важна: компонент, который читает только пользователя, после добавления в корзину не перерисуется.
Redux Toolkit: тот же Redux без шаблонного кода
Вручную копировать вложенные объекты утомительно, а на каждое действие раньше писали тип, функцию-создатель и ветку в switch. Redux Toolkit — официальный способ писать на Redux сегодня, и он убирает этот шаблонный код. createSlice по одному описанию делает редьюсер и создатели действий, а внутри работает библиотека Immer: в редьюсере можно писать push, и Immer соберёт из этого новое неизменяемое состояние.
import { createSlice, configureStore } from "@reduxjs/toolkit";
const cartSlice = createSlice({
name: "cart",
initialState: { items: [] },
reducers: {
added(state, action) {
state.items.push(action.payload);
},
removed(state, action) {
state.items = state.items.filter((item) => item !== action.payload);
},
},
});
export const { added, removed } = cartSlice.actions;
export const store = configureStore({ reducer: { cart: cartSlice.reducer } });
added("кружка") возвращает готовое действие { type: "cart/added", payload: "кружка" }. configureStore сразу подключает инструменты разработчика и в режиме разработки замораживает состояние: случайная мутация вне редьюсера упадёт с ошибкой, а не потеряется молча. Работает это только внутри createSlice: в обычной функции push по-прежнему мутирует.
Селекторы и лишние перерисовкиспросят на собеседовании
Самая частая проблема с Redux — компонент перерисовывается на каждое действие, хотя его данные не менялись. Обычно виноват селектор, который каждый раз создаёт новый массив: filter и map всегда возвращают новую ссылку, и === считает результат изменившимся.
Так не надо:
const inStock = useSelector((state) => state.items.filter((item) => item.inStock));
Так надо: запомнить результат и пересчитывать его, только когда изменились входные данные. В Redux Toolkit для этого есть createSelector, а ниже та же идея в десяти строках:
живой пример
let calls = 0;
const state = { items: [{ name: "чайник", inStock: true }, { name: "кружка", inStock: false }] };
const selectInStock = (s) => s.items.filter((item) => item.inStock);
function memoize(selector) {
let lastInput;
let lastResult;
return (s) => {
if (s.items !== lastInput) {
calls++;
lastInput = s.items;
lastResult = selector(s);
}
return lastResult;
};
}
console.log("без памяти, ссылки равны:", selectInStock(state) === selectInStock(state));
const selectMemo = memoize(selectInStock);
console.log("с памятью, ссылки равны:", selectMemo(state) === selectMemo(state));
console.log("фильтр посчитан раз:", calls);
Запустить
Запуск примеров доступен в платном доступе. Там этот же код выполняется прямо в статье: редактор, запуск и проверка рядом с абзацем. Три дня бесплатно →
RTK Query: данные с сервера внутри Reduxспросят на собеседовании
Список товаров с сервера — не состояние приложения, а кеш: его нужно загрузить, показать загрузку, обновить после изменения. Писать для этого действия и редьюсеры вручную — сотни строк. RTK Query входит в Redux Toolkit и решает ту же задачу, что TanStack Query: описываете запросы, получаете готовые хуки с флагами загрузки и кешем.
import { createApi, fetchBaseQuery } from "@reduxjs/toolkit/query/react";
export const shopApi = createApi({
baseQuery: fetchBaseQuery({ baseUrl: "/api" }),
tagTypes: ["Product"],
endpoints: (build) => ({
getProducts: build.query({ query: () => "products", providesTags: ["Product"] }),
addProduct: build.mutation({
query: (product) => ({ url: "products", method: "POST", body: product }),
invalidatesTags: ["Product"],
}),
}),
});
export const { useGetProductsQuery, useAddProductMutation } = shopApi;
Теги связывают запросы: после addProduct всё, что помечено Product, загрузится заново. Если в проекте уже есть Redux, RTK Query естественнее, чем вторая библиотека. Если Redux нет, проще взять TanStack Query: он не требует стора.
MobX: меняем объект, интерфейс следит самспросят на собеседовании
MobX устроен наоборот. Состояние — обычные объекты и классы, их меняют напрямую: cart.count = 3. Компонент, обёрнутый в observer, во время отрисовки запоминает, какие поля он прочитал, и перерисовывается только при их изменении. Селекторы и сравнение ссылок не нужны.
Механика держится на перехвате чтений. Вот она в упрощённом виде, на Proxy:
живой пример
let running = null;
function observable(target) {
const readers = new Map();
return new Proxy(target, {
get(obj, key) {
if (running) {
if (!readers.has(key)) readers.set(key, new Set());
readers.get(key).add(running);
}
return obj[key];
},
set(obj, key, value) {
obj[key] = value;
for (const reaction of readers.get(key) ?? []) reaction();
return true;
},
});
}
function autorun(fn) {
const reaction = () => {
running = reaction;
fn();
running = null;
};
reaction();
}
const cart = observable({ count: 0, promo: "" });
autorun(() => console.log("в корзине:", cart.count));
cart.count = 2;
cart.promo = "SALE";
cart.count = 3;
Запустить
Запуск примеров доступен в платном доступе. Там этот же код выполняется прямо в статье: редактор, запуск и проверка рядом с абзацем. Три дня бесплатно →
Изменение promo реакцию не запустило: её функция это поле не читала. В настоящем MobX то же самое делают makeAutoObservable и observer, а геттеры становятся вычисляемыми значениями с кешем:
import { makeAutoObservable } from "mobx";
import { observer } from "mobx-react-lite";
class CartStore {
items = [];
constructor() {
makeAutoObservable(this);
}
add(item) {
this.items.push(item);
}
get count() {
return this.items.length;
}
}
export const cart = new CartStore();
export const CartBadge = observer(() => <span>{cart.count}</span>);
Главная грабля MobX — забыть observer: компонент отрисуется один раз и больше не обновится. Вторая — прочитать поле вне отрисовки, например сохранить cart.count в переменную в начале модуля: такое чтение никто не отслеживает.
Как выбрать
Для нового проекта на React вопрос обычно решается так. Данные с сервера — TanStack Query или RTK Query, это не задача стора. Немного глобального клиентского состояния — Zustand: меньше всего кода. Большое приложение с командой и строгими правилами — Redux Toolkit: явные действия, история изменений в инструментах разработчика, предсказуемость. Сложная предметная модель с вычисляемыми полями, где удобно думать объектами, — MobX.
Коротко
- Redux: состояние неизменяемо, компонент перерисовывается, когда
===видит новую ссылку. - Мутация в обычном редьюсере не видна подписчикам; в
createSliceеё превращает в копию Immer. filterиmapв селекторе дают новую ссылку и лишние перерисовки — нуженcreateSelector.- RTK Query — кеш серверных данных внутри Redux; теги
providesTagsиinvalidatesTagsперезагружают связанные запросы. - MobX: объекты меняют напрямую,
observerследит за прочитанными полями; безobserverкомпонент не обновится. - Сервер — TanStack Query или RTK Query, мало клиентского состояния — Zustand, большая команда — Redux Toolkit, сложная модель — MobX.
Что почитать дальше
- Состояние: локальное и серверное — когда стор не нужен и хватает
useStateи context. - Загрузка данных: TanStack Query — кеш серверных данных без Redux.
- Модель отрисовки React — что вызывает перерисовку и как работает сравнение.
- Производительность —
memo,useMemoи поиск лишних перерисовок.