Перейти к содержимому
Тема 8 из 13
Теория

useContext: данные для всего приложения

Проблема: props через десять уровней

Данные в React текут сверху вниз через props. Обычно это хорошо: видно, откуда что пришло. Но некоторые данные нужны почти всем компонентам: текущий пользователь, тема оформления, язык, корзина. Передавать их через props приходится через каждый промежуточный компонент — даже через те, которым они не нужны:

function App() {
  const [user, setUser] = useState({ name: "Аня" });
  return <Layout user={user} />;
}
function Layout({ user }) {
  return <Header user={user} />;        // Layout сам user не использует
}
function Header({ user }) {
  return <Menu user={user} />;          // и Header тоже
}
function Menu({ user }) {
  return <p>Привет, {user.name}</p>;    // нужен только здесь
}

Это называют prop drilling («сверление» props). Код становится хрупким: чтобы добавить новое значение, нужно менять все промежуточные компоненты.

Контекст позволяет компоненту наверху «объявить» значение, а любому компоненту ниже — прочитать его напрямую, без передачи через промежуточные уровни.

Три шага: создать, передать, прочитать

const { createContext, useContext } = React;

// 1. Создать контекст (обычно в отдельном файле). Аргумент — значение по умолчанию
const UserContext = createContext(null);

// 2. Передать значение: обернуть часть дерева в Provider
function App() {
  const [user, setUser] = useState({ name: "Аня" });
  return (
    <UserContext.Provider value={user}>
      <Layout />
    </UserContext.Provider>
  );
}

function Layout() {
  return <Header />;          // никаких props
}
function Header() {
  return <Menu />;
}

// 3. Прочитать в любом компоненте внутри Provider
function Menu() {
  const user = useContext(UserContext);
  return <p>Привет, {user.name}</p>;
}
  • createContext(значениеПоУмолчанию) создаёт объект контекста.
  • <Ctx.Provider value={...}> делает значение доступным всем компонентам внутри, на любой глубине.
  • useContext(Ctx) возвращает значение ближайшего Provider выше по дереву. Если Provider'а нет — значение по умолчанию.

Когда значение меняется

Когда меняется value у Provider'а, React перерисовывает все компоненты, которые читают этот контекст через useContext, — даже если их родители не перерисовывались. Так контекст остаётся актуальным во всём приложении.

Provider'ы можно вкладывать: внутренний «перекрывает» внешний для своей части дерева.

<ThemeContext.Provider value="dark">
  <Page />                          {/* тут тема dark */}
  <ThemeContext.Provider value="light">
    <Footer />                      {/* а тут light */}
  </ThemeContext.Provider>
</ThemeContext.Provider>

Контекст + состояние

Сам по себе контекст ничего не хранит — он только передаёт значение вниз. Чтобы данные можно было менять, контекст соединяют с состоянием и передают вниз и значение, и функции для изменения:

const AuthContext = createContext(null);

function AuthProvider({ children }) {
  const [user, setUser] = useState(null);

  const login = (name) => setUser({ name });
  const logout = () => setUser(null);

  return (
    <AuthContext.Provider value={{ user, login, logout }}>
      {children}
    </AuthContext.Provider>
  );
}

function LoginButton() {
  const { user, login, logout } = useContext(AuthContext);
  return user
    ? <button onClick={logout}>Выйти ({user.name})</button>
    : <button onClick={() => login("Аня")}>Войти</button>;
}

function App() {
  return (
    <AuthProvider>
      <Header />
      <LoginButton />
    </AuthProvider>
  );
}

Это стандартная схема: компонент-«провайдер» с состоянием внутри и children снаружи. Через children всё приложение оказывается внутри Provider'а.

Свой хук для контекста

В проектах почти всегда делают маленький хук-обёртку. Он короче, и он сразу сообщает об ошибке, если компонент случайно оказался вне провайдера:

function useAuth() {
  const value = useContext(AuthContext);
  if (value === null) {
    throw new Error("useAuth must be used inside AuthProvider");
  }
  return value;
}

function Profile() {
  const { user } = useAuth();      // вместо useContext(AuthContext)
  return <p>{user ? user.name : "Гость"}</p>;
}

Хук — это обычная функция, имя которой начинается с use и внутри которой вызываются другие хуки. Подробнее о своих хуках — в теме про загрузку данных.

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

Контекст удобен, но не стоит класть в него всё подряд:

  • если данные нужны одному-двум уровням ниже — передай props, это проще и нагляднее;
  • часто помогает композиция: вместо передачи данных через Layout передай готовый элемент через children — <Layout><Menu user={user} /></Layout>;
  • каждое изменение value перерисовывает всех читателей. Для часто меняющихся данных (позиция мыши, текст в поле) контекст — плохой выбор.

Хорошие кандидаты для контекста: текущий пользователь, тема, язык, корзина, настройки.

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

  1. Компонент вне Provider. useContext молча вернёт значение по умолчанию (null), и где-то дальше будет Cannot read properties of null. Хук с проверкой выдаст понятную ошибку сразу.
  2. Передать в useContext не тот объект. Нужно useContext(ThemeContext), а не useContext(ThemeContext.Provider) или строку.
  3. Забыть value. <Ctx.Provider> без value передаст undefined.
  4. Ожидать, что изменение объекта внутри value обновит экран. Как и с состоянием: нужен новый объект через set….

Итог

  • Контекст передаёт данные вглубь дерева без props: createContext → <Ctx.Provider value> → useContext(Ctx).
  • useContext берёт значение ближайшего Provider выше, иначе — значение по умолчанию.
  • При изменении value перерисовываются все читатели контекста.
  • Контекст + useState в компоненте-провайдере = общее изменяемое состояние приложения.
  • Свой хук useX() с проверкой на null — удобно и безопасно.
  • Для данных на 1–2 уровня — обычные props.

Кнопка в цвет темы

Лёгкая+10 XP

Контекст ThemeContext уже создан (по умолчанию "light"). Напиши компонент ThemeButton с children: он рисует <button> с этим текстом, а класс кнопки (className) берёт из контекста.

<ThemeContext.Provider value="dark">
  <ThemeButton>OK</ThemeButton>       → <button class="dark">OK</button>
</ThemeContext.Provider>
<ThemeButton>OK</ThemeButton>         → <button class="light">OK</button>   (без Provider)

Подключи коннектор, чтобы решать задачи

Код задач выполняется на твоём компьютере — так быстрее и надёжнее, чем в браузере. Для этого нужна бесплатная программа-коннектор CodeWithMe. Подключить её нужно один раз.

Ищем коннектор на этом компьютере…

  1. 1
    Скачай коннектор для своей системы (Windows, macOS или Linux).Скачать коннекторНа странице загрузки выбери файл со своей системой в названии, например windows-amd64.
  2. 2Запусти его и выбери папку, где будут лежать твои решения. Python и Node.js он поставит туда сам, когда понадобится.
  3. 3
    Введи ниже код из 6 цифр — он написан в окне коннектора («Код для сайта CodeWithMe»). Если браузер спросит про доступ к локальной сети — разреши.

Коннектор открытый (исходный код на GitHub), работает только на твоём компьютере и выполняет код лишь после ввода кода сопряжения.