Урок по треку React 7 мин

Context и стейт-менеджеры без мифов

Context, его цена для рендера и выбор между Redux, Zustand и серверным кешем.

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

Четыре вида состояния

  1. Локальное UI-состояние: открыт ли дропдаун, текст в поле. Живёт в useState у компонента.
  2. Общее клиентское состояние: тема, текущий пользователь, корзина. Нужно многим компонентам в разных ветках дерева.
  3. Серверное состояние: данные с бэкенда. Это кеш чужих данных: он устаревает, его надо перезапрашивать, дедуплицировать запросы, показывать загрузку и ошибки.
  4. Состояние в URL: фильтры, номер страницы, открытая вкладка. Его можно скопировать ссылкой и пережить перезагрузку.

Типичная ошибка: класть всё в один глобальный стор, включая ответы API и флаг открытой модалки.

Сначала без библиотек

Если состояние нужно двум соседям, поднимите его к ближайшему общему родителю. Если пропсы приходится тащить через 4-5 уровней (prop drilling), сначала попробуйте композицию: передайте готовый элемент через children, и промежуточные слои перестанут знать про данные.

// вместо <Layout user={user}> ... <Header user={user}> ... <Avatar user={user}>
<Layout header={<Header avatar={<Avatar user={user} />} />} />

Context: транспорт, а не стор

Context доставляет значение вниз по дереву без пропсов. В React 19 провайдером служит сам контекст:

const ThemeContext = createContext('light');

function App() {
  const [theme, setTheme] = useState('light');
  return (
    <ThemeContext value={theme}>
      <Page />
    </ThemeContext>
  );
}

function Button() {
  const theme = use(ThemeContext); // или useContext(ThemeContext)
  return <button className={theme}>OK</button>;
}

Запись <ThemeContext.Provider> тоже работает, но в новых проектах пишут короче.

Ключевое правило: когда value провайдера меняется (сравнение через Object.is), перерисовываются все компоненты, которые читают этот контекст, даже если им нужна только часть значения. React.memo на потребителе не спасает: подписка на контекст пробивает memo.

Отсюда частые ошибки:

// новый объект на каждый рендер App: все потребители рендерятся всегда
<AuthContext value={{ user, login, logout }}>

Лечится так:

  • мемоизировать значение useMemo (или довериться React Compiler, если он включён);
  • разделить контексты: данные отдельно, функции-сеттеры отдельно (сеттеры стабильны и почти никогда не меняются);
  • не класть в контекст часто меняющееся значение (позиция мыши, ввод в поле), которое читает пол-приложения.

Context хорош для редко меняющихся вещей: тема, локаль, текущий пользователь, зависимости (клиент API).

Внешние сторы

Когда общего состояния много и оно часто меняется, берут стор вне React. Компонент подписывается на срез и перерисовывается только при изменении этого среза.

Redux Toolkit (RTK): единый стор, изменения только через экшены и редьюсеры. createSlice генерирует экшены, внутри редьюсера можно «мутировать» черновик, потому что RTK использует Immer. Сильные стороны: предсказуемость, DevTools с историей экшенов, middleware. RTK Query закрывает серверное состояние.

const cart = createSlice({
  name: 'cart',
  initialState: { items: [] },
  reducers: {
    added(state, action) { state.items.push(action.payload); }, // Immer
  },
});
const count = useSelector((s) => s.cart.items.length);

Zustand: стор как хук, без провайдера и бойлерплейта.

const useCart = create((set) => ({
  items: [],
  add: (item) => set((s) => ({ items: [...s.items, item] })),
}));
const count = useCart((s) => s.items.length); // подписка на срез

Атомарные (Jotai, Recoil в прошлом): состояние разбито на маленькие атомы, компонент подписан только на свои атомы.

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

Под капотом корректные внешние сторы используют useSyncExternalStore: он даёт подписку без «разрывов» (tearing) в конкурентном рендере.

Серверное состояние отдельно

TanStack Query, RTK Query, SWR решают задачи кеша: ключ запроса, дедупликация, повторный запрос при фокусе окна, инвалидация после мутации, ретраи. В TanStack Query v5 по умолчанию staleTime: 0 (данные сразу считаются устаревшими) и gcTime 5 минут (сколько неиспользуемый кеш живёт в памяти; раньше назывался cacheTime).

Писать загрузку через useEffect + useState + глобальный стор можно, но придётся заново изобрести гонки, отмену и кеш.

Частые ошибки

  • Context с объектом-литералом в value и десятком потребителей.
  • Ожидание, что React.memo остановит перерисовку от контекста.
  • Селектор, который возвращает новый объект: useStore((s) => ({ a: s.a, b: s.b })) в Zustand v5 без useShallow вызывает лишние рендеры и даже бесконечный цикл.
  • Копия данных сервера в Redux, а потом ручная синхронизация.
  • Глобальный стор для формы, которая живёт на одном экране.

Что спросят на собесе

  • Когда хватит useState и подъёма состояния, а когда нужен Context или стор?
  • Почему Context вызывает лишние перерисовки и как это исправить?
  • Чем Redux Toolkit отличается от Zustand, и что выбрать для нового проекта?
  • Что такое серверное состояние и почему его не держат в Redux?
  • Зачем нужен useSyncExternalStore?

Закрепите за 5 минут

После урока в тренажёре 3-5 проверочных вопросов, дальше модуль уходит в повторения.