Menu

Хук useContext в React: общие данные без пропсов

useContext читает значение, которое предоставляет родительский компонент, поэтому глубоко вложенный компонент получает его без того, чтобы каждый компонент между ними передавал его вниз пропсом. createContext, провайдеры, обновление контекста из дочернего компонента и как контекст влияет на повторные рендеры.

На этой странице есть исполняемые редакторы: меняйте, запускайте и сразу видите результат.

useContext читает значение, которое предоставляет родительский компонент, поэтому компонент глубоко в дереве может его получить без того, чтобы каждый компонент между ними передавал его пропсом. Вы создаёте контекст через createContext, оборачиваете часть дерева в провайдер с value и вызываете useContext(TheContext) везде, где нужно это значение.

Toolbar ни разу не упоминает тему, но обе кнопки меняются, когда вы ставите флажок. В этом весь смысл контекста: компоненты посередине не несут пропс, который не используют, и это проблема prop drilling, которую решает контекст.

Три шага

// 1. Create it once, outside any component, usually in its own file
export const ThemeContext = createContext('light');

// 2. Provide a value to a part of the tree
<ThemeContext value={theme}>
    <Page />
</ThemeContext>

// 3. Read it in any component inside that part
const theme = useContext(ThemeContext);

В React 19 сам объект контекста является провайдером. Старый код пишет <ThemeContext.Provider value={theme}>, что по-прежнему работает, поэтому вы увидите оба варианта.

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

Значение по умолчанию

Аргумент createContext это то, что возвращает useContext, когда над компонентом нет провайдера. Оно не меняется при изменении состояния; это запасной вариант.

Над первым Greeting нет провайдера, и он получает значение по умолчанию 'en'. Второй читает 'es', а третий читает более близкий 'ja'. Замените внешний value="es" на value="ja", и изменится только вторая строка.

Обновление контекста из дочернего компонента

Контекст только передаёт значение вниз. Чтобы глубокий дочерний компонент мог его изменить, храните значение в состоянии там, где провайдер, и положите сеттер в контекст рядом с ним.

LoginButton вызывает setUser, состояние в App меняется, провайдер получает новое значение, и Header тоже обновляется. Ни один компонент не получил пропс.

Каждый потребитель перерендеривается при изменении значения

Когда value провайдера меняется, React рендерит каждый компонент, который вызывает для него useContext, даже если этот компонент находится внутри memo и его пропсы не изменились. React сравнивает старое и новое значение через Object.is. Поэтому value={{ user, setUser }} при каждом рендере App это новый объект, и каждый потребитель рендерится снова, даже если user не изменился.

Оба читателя выводят сообщение один раз при первом рендере. Нажмите кнопку: снова выводит только PlainReader, потому что его контекст получил новый объект. useMemo передаёт MemoContext один и тот же объект, пока не изменится name, поэтому MemoReader остаётся на месте. Это важно, только когда рендеринг становится медленным; для небольшого приложения новый объект на каждый рендер это нормально.

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

Значение, которое меняется часто, и значение, которое никогда не меняется, не должны делить один контекст. Если сеттер живёт вместе с состоянием, компонент, которому нужно только вызвать setUser, всё равно рендерится каждый раз, когда меняется user. Разнесите их по двум контекстам, и каждый потребитель будет подписан только на то, что читает:

const UserContext = createContext(null);
const SetUserContext = createContext(() => {});

function UserProvider({ children }) {
    const [user, setUser] = useState(null);
    return (
        <SetUserContext value={setUser}>
            <UserContext value={user}>{children}</UserContext>
        </SetUserContext>
    );
}

setUser из useState навсегда сохраняет одну и ту же идентичность, поэтому компоненты, которые читают только SetUserContext, никогда не рендерятся из-за входа пользователя. Та же идея применима к несвязанным данным: тема и корзина покупок принадлежат отдельным контекстам, а не одному большому AppContext.

Частый завершающий штрих это пользовательский хук, например useUser(), который вызывает useContext(UserContext) и выбрасывает понятную ошибку, когда провайдера нет, чтобы вызывающие никогда не импортировали объект контекста напрямую.

Контекст, пропсы или библиотека состояния

  • Пропсы это вариант по умолчанию. Передать значение на два или три уровня вниз понятно и легко отследить.
  • Контекст подходит для значений, которые нужны многим компонентам на разной глубине и меняются редко: тема, язык, вошедший пользователь, объект флагов функций.
  • Библиотека состояния (Redux Toolkit, Zustand, Jotai и другие) подходит для большого часто обновляемого состояния, где компонент должен подписываться на один срез и пропускать рендеры, вызванные остальным. У контекста такого селектора нет: потребитель рендерится при любом изменении значения.

Прежде чем браться за контекст или библиотеку, проверьте, не убирает ли drilling передача компонентов как children. Часто убирает, и вообще без нового API.

Часто задаваемые вопросы

Что делает useContext в React?

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

Для чего значение по умолчанию в createContext?

Это то, что возвращает useContext, когда над компонентом нет провайдера. Оно полезно для тестов и для компонентов, отрендеренных отдельно, и никогда не меняется.

Нужен ли Context.Provider в React 19?

Нет. В React 19 можно рендерить сам контекст как провайдер: <ThemeContext value="dark">. <ThemeContext.Provider value="dark"> по-прежнему работает, поэтому старый код продолжает работать.

Как обновить значение контекста из дочернего компонента?

Храните значение в состоянии в компоненте, который рендерит провайдер, и положите сеттер в контекст рядом со значением: value={{ theme, setTheme }}. Дочерний компонент читает setTheme через useContext и вызывает его.

Заменяет ли контекст React библиотеку Redux?

Контекст передаёт значение вниз по дереву; сам он состоянием не управляет. Вместе с useState или useReducer он покрывает многие приложения. Библиотека состояния добавляет то, чего у контекста нет, например подписку на один срез, чтобы несвязанные обновления не перерендеривали компонент, а также devtools и middleware.

Иллюстрация языков программирования Coddy

Учитесь программировать с Coddy

НАЧАТЬ