Kodokon kodokon.com

Контекст: делись данными без prop drilling

Передавай данные без prop drilling с помощью контекста, держа под контролем его перерисовки и ограничения.

8 мин · 3 вопросов

Открыть этот урок в Kodokon

Prop drilling - это те пропсы, которые проходят через три-четыре промежуточных компонента, не используясь в них, только чтобы добраться до листа дерева. Каждый слой, который они пересекают, оказывается связан с данными, до которых ему нет дела. Контекст решает эту проблему транспорта: createContext создаёт канал, провайдер публикует значение, а useContext читает его из любого потомка. Держи в голове нюанс: контекст переносит состояние, но не управляет им - значение по-прежнему живёт в компоненте, здесь в провайдере.

JSX
import { createContext, useMemo, useState } from "react";

const ThemeContext = createContext(null);

function ThemeProvider({ children }) {
  const [theme, setTheme] = useState("dark");
  const value = useMemo(
    () => ({ theme, setTheme }),
    [theme]
  );

  return (
    <ThemeContext.Provider value={value}>
      {children}
    </ThemeContext.Provider>
  );
}
Публикуемое значение мемоизировано, чтобы его ссылка оставалась стабильной.

Когда значение провайдера меняется, перерисовываются все потребители, даже глубоко вложенные - и React.memo их не защитит, потому что он сравнивает пропсы, а не контекст. Классическая ловушка только усугубляет дело: запись value={{ theme, setTheme }} создаёт новый объект при каждом рендере провайдера, а значит новую ссылку, а значит каскад перерисовок, даже если theme не сдвинулся с места. Именно для этого нужен useMemo выше. Дополни конструкцию хуком доступа, который громко падает за пределами провайдера.

JSX
import { useContext } from "react";

function useTheme() {
  const context = useContext(ThemeContext);
  if (context === null) {
    throw new Error(
      "useTheme must be inside <ThemeProvider>"
    );
  }
  return context;
}
Явная ошибка лучше молчаливого null.

Прежде чем создавать контекст, исчерпай вариант с композицией. Если промежуточный слой только передаёт пропс дальше, переверни структуру: собери элемент наверху дерева, там, где живут данные, и передай его через children или отдельный пропс. Промежуточный слой становится универсальным, а данные больше не проходят ни через один компонент, который их не использует.

JSX
function App({ currentUser }) {
  return (
    <Layout
      header={<TopBar user={currentUser} />}
      content={<Dashboard />}
    />
  );
}
Layout ничего не знает о currentUser: он просто вставляет готовые элементы.

Проверка знаний

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

  1. Потребитель контекста обёрнут в React.memo, и значение контекста меняется. Что произойдёт?
    • Он не перерисуется: React.memo блокирует все обновления
    • Он перерисуется: React.memo сравнивает пропсы, а не значения контекста
    • Он перерисуется, только если изменятся и его пропсы
  2. Зачем мемоизировать объект, который передаётся в value провайдера?
    • Чтобы ускорить создание объекта при каждом рендере
    • Потому что провайдер требует неизменяемое значение
    • Чтобы ссылка менялась только при изменении данных, избегая лишних перерисовок потребителей
  3. В чём главное ограничение контекста для состояния, которое обновляется очень часто?
    • Нет селекторов: каждое обновление перерисовывает всех потребителей, даже тех, кто использует лишь часть значения
    • Контекст асинхронен и задерживает обновления
    • Контекст может хранить только примитивные значения