Context правильно: без лишних ре-рендеров

Урок про правильную работу с Context: как давать данные «вглубь» дерева без prop drilling и не плодить лишних ре-рендеров.

Context — механизм передачи значения всем потомкам без передачи через пропсы на каждом уровне. Но всякое изменение значения контекста перерисовывает всех подписчиков.

Зачем контекст

Когда данные (текущий пользователь, тема оформления, локаль) нужны во многих местах дерева, протаскивать их пропсами через десяток промежуточных компонентов — prop drilling — больно. Контекст даёт значение напрямую тем, кто его читает через useContext.

const ThemeContext = React.createContext("light");

function App() {
  const [theme, setTheme] = React.useState("light");
  return (
    <ThemeContext.Provider value={theme}>
      <Toolbar />
    </ThemeContext.Provider>
  );
}

function ThemedButton() {
  const theme = React.useContext(ThemeContext); // без пропсов
  return <button className={theme}>Кнопка</button>;
}

Главная ловушка: лишние ре-рендеры

Когда value провайдера меняется, перерисовываются все компоненты, читающие этот контекст. Две частые ошибки усугубляют это:

1. Новый объект value на каждый рендер

// ПЛОХО: новый объект каждый рендер ⇒ все потребители перерисовываются всегда
<AuthContext.Provider value={{ user, login, logout }}>

Объект-литерал создаётся заново каждый рендер, ссылка всегда новая. Стабилизируйте его через useMemo:

const value = React.useMemo(() => ({ user, login, logout }), [user, login, logout]);
return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>;

2. Один «толстый» контекст на всё

Если запихнуть в один контекст и тему, и пользователя, и корзину, то изменение корзины перерисует и тех, кому нужна только тема. Лекарство — разделить контексты по частоте изменений и смыслу.

Разделение контекстов

Частый приём — разнести данные и действия в два контекста. Действия (dispatch, функции) стабильны и почти не меняются, поэтому компоненты, которым нужны только действия, не перерисовываются при изменении данных.

const StateContext = React.createContext(null);
const DispatchContext = React.createContext(null);

function Store({ children }) {
  const [state, dispatch] = React.useReducer(reducer, initial);
  return (
    <StateContext.Provider value={state}>
      <DispatchContext.Provider value={dispatch}>
        {children}
      </DispatchContext.Provider>
    </StateContext.Provider>
  );
}

Компонент, который только отправляет действия (кнопка «добавить»), читает DispatchContext — а dispatch стабилен, значит, лишних рендеров нет.

Когда контекст не нужен

  • Данные нужны двум соседним компонентам — проще поднять состояние и передать пропсом.
  • Значение меняется очень часто (например, позиция курсора) и читается многими — контекст вызовет шквал рендеров; подумайте о внешнем сторе или локализации обновлений.
  • Контекст — не замена менеджеру состояния. Он лишь доставляет значение; всю сложную логику по-прежнему держите в useReducer/сторе.

Итог

  • Контекст устраняет prop drilling, давая значение потомкам напрямую.
  • Стабилизируйте value через useMemo — иначе потребители рендерятся всегда.
  • Разделяйте контексты по смыслу/частоте; выносите стабильный dispatch отдельно.
  • Для пары соседей хватит подъёма состояния — не каждый случай требует контекста.
Проверьте себя
1. Какую проблему решает Context?
AУскоряет рендеринг автоматически
BПередаёт значение потомкам без prop drilling через все уровни
CЗаменяет useReducer
DКэширует сетевые запросы
2. Почему `value={{ user, login, logout }}` в провайдере — плохо?
AОбъекты нельзя класть в контекст
BОбъект-литерал создаётся заново каждый рендер, и все потребители перерисовываются
CЭто синтаксическая ошибка
DКонтекст не поддерживает функции
3. Зачем разносить state и dispatch в два разных контекста?
AТак требует синтаксис React
BЧтобы компоненты, которым нужны только действия, не рендерились при изменении данных
CЧтобы уменьшить размер бандла
DЭто ничего не меняет
Поддержать проект