Передача данных через контекст
Обычно вы передаёте информацию от родительского компонента к дочернему с помощью пропсов. Однако такая передача может стать многослойной и неудобной, если необходимо передавать информацию через большое количество промежуточных компонентов или если множеству компонентам в вашем приложении нужна одна и та же информация. Контекст позволяет родительскому компоненту предоставлять информацию любому компоненту в дереве под ним, независимо от глубины и не передавая данные явно через пропсы.
Вы узнаете
- Что такое “проп бурение”
- Как заменить повторяющуюся передачу пропсов
- Рядовые случаи использования контекста
- Альтернативы контекста
Проблема передачи пропа
Передача пропсов — это отличный способ явно передать данные через UI дерево компонентам, которые их используют.
Однако передача пропсов может стать муторной, если вам нужно передать их глубоко в дерево или если множеству компонентам нужен один и тот же проп. Ближайший общий предок может находиться далеко от компонентов, которым нужны данные, и подъём состояния вверх на такую высоту может привести к ситуации, называемой “проп бурение”.
А если представить, что у нас есть возможность “телепортировать” данные в те компоненты дерева, которым они нужны, без передачи пропсов? С помощью функции React Контекст это возможно!
Контекст: альтернатива передачи пропсов
Контекст позволяет родительскому компоненту передавать данные всему дереву под ним. Существует множество вариантов использования контекста. Вот один из примеров. Рассмотрим компонент Heading
, который принимает значение level
для своего размера:
import Heading from './Heading.js'; import Section from './Section.js'; export default function Page() { return ( <Section> <Heading level={1}>Наименование</Heading> <Heading level={2}>Заголовок</Heading> <Heading level={3}>Под-заголовок</Heading> <Heading level={4}>Под-под-заголовок</Heading> <Heading level={5}>Под-под-под-заголовок</Heading> <Heading level={6}>Под-под-под-под-заголовок</Heading> </Section> ); }
Допустим, вы хотите, чтобы несколько заголовков в одном Section
всегда имели одинаковый размер:
import Heading from './Heading.js'; import Section from './Section.js'; export default function Page() { return ( <Section> <Heading level={1}>Наименование</Heading> <Section> <Heading level={2}>Заголовок</Heading> <Heading level={2}>Заголовок</Heading> <Heading level={2}>Заголовок</Heading> <Section> <Heading level={3}>Под-заголовок</Heading> <Heading level={3}>Под-заголовок</Heading> <Heading level={3}>Под-заголовок</Heading> <Section> <Heading level={4}>Под-под-заголовок</Heading> <Heading level={4}>Под-под-заголовок</Heading> <Heading level={4}>Под-под-заголовок</Heading> </Section> </Section> </Section> </Section> ); }
Сейчас вы передаёте проп level
каждому <Heading>
отдельно:
<Section>
<Heading level={3}>О нас</Heading>
<Heading level={3}>Фото</Heading>
<Heading level={3}>Видео</Heading>
</Section>
Более удобным будет передавать параметр level
в компонент <Section>
и убирать его из <Heading>
. Таким образом, мы можем добиться того, чтобы все заголовки в одном разделе имели одинаковый размер:
<Section level={3}>
<Heading>О нас</Heading>
<Heading>Фото</Heading>
<Heading>Видео</Heading>
</Section>
Но как компонент <Heading>
может узнать уровень ближайшего к нему <Section>
? Это потребует от дочернего компонента какого-то способа “запрашивать” данные откуда-то сверху.
С помощью одних только пропсов этого не сделать. Здесь на помощь приходит контекст. Вы можете сделать это в три шага:
- Создать контекст. (Можно назвать его
LevelContext
, поскольку он предназначен для уровня заголовка). - Использовать этот контекст в компоненте, которому нужны данные. (
Heading
будет использоватьLevelContext
). - Передать этот контекст компоненту, определяющему данные. (
Section
передастLevelContext
).
Контекст позволяет родительскому компоненту — даже удалённому — предоставлять определённые данные всему дереву компонентов внутри него.
Шаг 1: Создать контекст
Сначала нужно создать контекст. Потом вам нужно будет экспортировать его из файла, чтобы ваши компоненты могли его использовать:
import { createContext } from 'react'; export const LevelContext = createContext(1);
Единственным аргументом createContext
является значение по умолчанию. Здесь 1
означает самый большой уровень заголовка, но вы можете передать любое значение (даже объект). Значимость значения по умолчанию вы увидите в следующем шаге.
Шаг 2: Использовать контекст
Импортируем хук useContext
и ваш контекст из React:
import { useContext } from 'react';
import { LevelContext } from './LevelContext.js';
Сейчас компонент Heading
считывает level
из пропсов:
export default function Heading({ level, children }) {
// ...
}
Вместо этого удалите проп level
и добавьте значение из контекста, который вы только что импортировали — LevelContext
:
export default function Heading({ children }) {
const level = useContext(LevelContext);
// ...
}
useContext
— это хук. Как и useState
и useReducer
, его можно вызывать только непосредственно внутри компонента React (не в циклах или условиях). useContext
сообщает React, что компонент Heading
хочет получить данные из LevelContext
.
Теперь, когда компонент Heading
больше не имеет свойство level
, вам не нужно передавать этот проп внутрь Heading
в ваш JSX как здесь:
<Section>
<Heading level={4}>Под-под-заголовок</Heading>
<Heading level={4}>Под-под-заголовок</Heading>
<Heading level={4}>Под-под-заголовок</Heading>
</Section>
Обновите JSX так, чтобы компонент Section
получал проп level
как в примере:
<Section level={4}>
<Heading>Под-под-заголовок</Heading>
<Heading>Под-под-заголовок</Heading>
<Heading>Под-под-заголовок</Heading>
</Section>
Вспомним, что это тот код, который вы пытались заставить работать:
import Heading from './Heading.js'; import Section from './Section.js'; export default function Page() { return ( <Section level={1}> <Heading>Наименование</Heading> <Section level={2}> <Heading>Заголовок</Heading> <Heading>Заголовок</Heading> <Heading>Заголовок</Heading> <Section level={3}> <Heading>Под-заголовок</Heading> <Heading>Под-заголовок</Heading> <Heading>Под-заголовок</Heading> <Section level={4}> <Heading>Под-под-заголовок</Heading> <Heading>Под-под-заголовок</Heading> <Heading>Под-под-заголовок</Heading> </Section> </Section> </Section> </Section> ); }
Обратите внимание, что этот пример еще не совсем рабочий! Все заголовки имеют одинаковый размер, потому что хоть вы и используете контекст, вы еще не указали его. React не знает, где его взять!
Если вы не укажете контекст, React будет использовать значение по умолчанию, которое вы указали на предыдущем шаге. В этом примере вы указали 1
в качестве аргумента для createContext
, поэтому useContext(LevelContext)
возвращает 1
, устанавливая для всех заголовков <h1>
это значение. Давайте исправим эту проблему, заставив каждый Section
передать свой собственный контекст.
Шаг 3: Указать контекст
Компонент Section
в данный момент отображает свои дочерние элементы:
export default function Section({ children }) {
return (
<section className="section">
{children}
</section>
);
}
Оберните их провайдером контекста, чтобы передать им LevelContext
:
import { LevelContext } from './LevelContext.js';
export default function Section({ level, children }) {
return (
<section className="section">
<LevelContext.Provider value={level}>
{children}
</LevelContext.Provider>
</section>
);
}
Это сообщает React: “если какой-либо компонент внутри <Section>
запрашивает LevelContext
, дайте ему этот уровень.” Компонент будет использовать значение ближайшего <LevelContext.Provider>
в дереве UI над ним.
import Heading from './Heading.js'; import Section from './Section.js'; export default function Page() { return ( <Section level={1}> <Heading>Наименование</Heading> <Section level={2}> <Heading>Заголовок</Heading> <Heading>Заголовок</Heading> <Heading>Заголовок</Heading> <Section level={3}> <Heading>Под-заголовок</Heading> <Heading>Под-заголовок</Heading> <Heading>Под-заголовок</Heading> <Section level={4}> <Heading>Под-под-заголовок</Heading> <Heading>Под-под-заголовок</Heading> <Heading>Под-под-заголовок</Heading> </Section> </Section> </Section> </Section> ); }
Это тот же результат, что и в исходном коде, но вам не нужно передавать проп level
каждому компоненту Heading
! Вместо этого он “выясняет” уровень своего заголовка, запрашивая ближайший Section
выше:
- Вы передаёте проп
level
в<Section>
. Section
оборачивает дочерние элементы в<LevelContext.Provider value={level}>
.Heading
запрашивает ближайшее значениеLevelContext
с помощьюuseContext(LevelContext)
.
Использование и передача контекста в компонентах
В настоящее время вам по-прежнему приходится указывать level
каждого раздела вручную:
export default function Page() {
return (
<Section level={1}>
...
<Section level={2}>
...
<Section level={3}>
...
Так как контекст позволяет считывать информацию из компонента выше, каждый Section
может считывать level
из Section
сверху, и автоматически передавать level + 1
вниз. Вот как это можно сделать:
import { useContext } from 'react';
import { LevelContext } from './LevelContext.js';
export default function Section({ children }) {
const level = useContext(LevelContext);
return (
<section className="section">
<LevelContext.Provider value={level + 1}>
{children}
</LevelContext.Provider>
</section>
);
}
Благодаря этому изменению вам не нужно передавать параметр level
какому-либо <Section>
или <Heading>
:
import Heading from './Heading.js'; import Section from './Section.js'; export default function Page() { return ( <Section> <Heading>Наименование</Heading> <Section> <Heading>Заголовок</Heading> <Heading>Заголовок</Heading> <Heading>Заголовок</Heading> <Section> <Heading>Под-заголовок</Heading> <Heading>Под-заголовок</Heading> <Heading>Под-заголовок</Heading> <Section> <Heading>Под-под-заголовок</Heading> <Heading>Под-под-заголовок</Heading> <Heading>Под-под-заголовок</Heading> </Section> </Section> </Section> </Section> ); }
Теперь и Heading
, и Section
читают LevelContext
, чтобы выяснить, насколько “глубоко” они находятся. А Section
оборачивает свои дочерние элементы в LevelContext с целью указать, что всё, что находится внутри, расположено на более “глубоком” уровне.
Прохождение контекста через промежуточные компоненты
Вы можете использовать столько компонентов между передающим контекст и тем, который его использует компонентами, сколько захотите. Сюда входят как базовые компоненты, такие как <div>
, так и те, которые вы можете создать самостоятельно.
В этом примере один и тот же компонент Post
(с пунктирной границей) отображается на двух разных уровнях вложенности. Обратите внимание, что <Heading>
внутри него автоматически получает свой уровень из ближайшего <Section>
:
import Heading from './Heading.js'; import Section from './Section.js'; export default function ProfilePage() { return ( <Section> <Heading>Мой профиль</Heading> <Post title="Привет, путешественник!" body="Почитай о моих путешествиях." /> <AllPosts /> </Section> ); } function AllPosts() { return ( <Section> <Heading>Пост</Heading> <RecentPosts /> </Section> ); } function RecentPosts() { return ( <Section> <Heading>Последние посты</Heading> <Post title="Вкусы Лиссабона" body="...those pastéis de nata!" /> <Post title="Буэнос-Айрес в ритме танго" body="Мне понравилось это!" /> </Section> ); } function Post({ title, body }) { return ( <Section isFancy={true}> <Heading> {title} </Heading> <p><i>{body}</i></p> </Section> ); }
Вы не сделали ничего волшебного, чтобы это заработало. Section
определяет контекст для дерева внутри него, поэтому вы можете поставить <Heading>
в любое место, и он будет иметь правильный размер. Попробуйте это в песочнице выше!
Контекст позволяет вам писать компоненты, которые “адаптируются к своему окружению” и отображаются по-разному в зависимости от того, где (или другими словами, в каком контексте) они отображаются.
То, как работает контекст, может напомнить вам о наследовании свойств CSS. В нём вы можете указать color: blue
для <div>
, и любой узел DOM внутри него, независимо от его глубины, унаследует этот цвет, если только какой-либо другой узел DOM в середине не переопределит его на color: green
. Аналогично в React. Eдинственный способ переопределить контекст, поступающий сверху, — это обернуть дочерние элементы в провайдер контекста с другим значением.
В CSS разные свойства, такие как color
и background-color
, не переопределяют друг друга. Вы можете установить во всех <div>
свойство color
на красный, не влияя на background-color
. Аналогично, разные контексты React не переопределяют друг друга. Каждый контекст, который вы создаете с помощью createContext()
полностью отделен от других и связывает компоненты, использующие и передающие этот конкретный контекст. Один компонент может использовать или передавать множество разных контекстов без проблем.
Перед использованием контекста
Контекст — это очень заманчиво! Однако это также означает, что им слишком легко злоупотребить. Если вам нужно просто передать какие-то пропсы на несколько уровней в глубину, это не значит, что вы должны передавать информацию через контекст.
Вот несколько альтернатив, которые нужно рассмотреть, прежде чем использовать контекст:
- Начните с передачи пропсов. Если ваши компоненты достаточно простые, то нередко приходится передавать множество пропсов вниз через множество компонентов. Это может показаться трудоёмкой задачей, но так становится ясно, какие компоненты используют те или иные данные! Человек, обслуживающий ваш код, будет рад, что вы сделали поток данных явным с помощью пропсов.
- Извлекайте компоненты и передавайте им JSX как
детям
. Если вы передаёте какие-то данные через множество промежуточных компонентов, которые не используют эти данные (а только передают их дальше вниз), это часто означает, что вы забыли извлечь некоторые компоненты на этом пути. Например, вы передаете такие пропсы, какposts
, визуальным компонентам, которые не используют их напрямую, например,<Layout posts={posts} />
. Вместо этого сделайте так, чтобыLayout
принималchildren
в качестве проп и выводил<Layout><Posts posts={posts} /></Layout>
. Это уменьшает количество слоёв между компонентом, задающим данные, и компонентом, которому они нужны.
Если ни один из этих подходов вам не подходит, рассмотрите контекст.
Варианты использования контекста
- Изменение темы: Если ваше приложение позволяет пользователю изменять его внешний вид (например, темный режим), вы можете поместить провайдер контекста в верхней части приложения и использовать этот контекст в компонентах, которым нужно изменять свой внешний вид.
- Текущий аккаунт: Многим компонентам может потребоваться информация о текущем вошедшем в систему пользователе. Поместив его в контекст, эту информацию удобно будет читать в любом месте дерева. Некоторые приложения также позволяют работать с несколькими учетными записями одновременно (например оставлять комментарии от имени другого пользователя). В таких случаях может быть удобно обернуть часть UI во вложенный провайдер с другим текущим значением.
- Маршрутизация: Большинство решений для маршрутизации используют внутренний контекст для хранения текущего маршрута. Так каждая ссылка “знает”, активна она или нет. Если вы создадите свой собственный маршрутизатор, то, возможно, захотите сделать также.
- Управление состоянием: По мере роста вашего приложения вы можете столкнуться с большим количеством состояний в верхней части вашего приложения. Многие дальние компоненты внизу могут захотеть изменить их. Обычно используется редюсер вместе с контекстом, чтобы управлять сложным состоянием и передавать его вниз удаленным компонентам без особых проблем.
Контекст не ограничивается статическими значениями. Если при следующем рендере вы передадите другое значение, React обновит все компоненты, читающие его ниже! Именно поэтому контекст часто используется в связке с состоянием.
В общем, если какая-то информация нужна удалённым компонентам в разных частях дерева, это хороший признак того, что контекст вам может помочь.
Recap
- Контекст позволяет компоненту передавать некоторую информацию всему дереву под ним.
- Чтобы передать контекст:
- Создайте и экспортируйте его с помощью
export const MyContext = createContext(defaultValue)
. - Передайте его хуку
useContext(MyContext)
чтобы прочитать его в любом дочернем компоненте, независимо от его глубины. - Заверните дочерние компоненты в обертку
<MyContext.Provider value={...}>
, чтобы подтянуть его из родительского компонента.
- Создайте и экспортируйте его с помощью
- Контекст проходит через любые компоненты в середине.
- Контекст позволяет писать компоненты, которые “адаптируются к своему окружению”.
- Прежде чем использовать контекст, попробуйте передать пропсы или передать JSX в качестве
children
.
Challenge 1 of 1: Замените проп бурение на контекст
В этом примере переключение checkbox
изменяет проп imageSize
, передаваемый каждому <PlaceImage>
. Состояние элемента checkbox
хранится в компоненте верхнего уровня App
, но каждый <PlaceImage>
должен знать о нем.
Сейчас App
передает imageSize
в List
, который передает его в каждый Place
, который передает его в PlaceImage
. Удалите проп imageSize
и вместо этого передавайте его из компонента App
прямо в PlaceImage
.
Вы можете объявить контекст в файле Context.js
.
import { useState } from 'react'; import { places } from './data.js'; import { getImageUrl } from './utils.js'; export default function App() { const [isLarge, setIsLarge] = useState(false); const imageSize = isLarge ? 150 : 100; return ( <> <label> <input type="checkbox" checked={isLarge} onChange={e => { setIsLarge(e.target.checked); }} /> Использовать большие изображения </label> <hr /> <List imageSize={imageSize} /> </> ) } function List({ imageSize }) { const listItems = places.map(place => <li key={place.id}> <Place place={place} imageSize={imageSize} /> </li> ); return <ul>{listItems}</ul>; } function Place({ place, imageSize }) { return ( <> <PlaceImage place={place} imageSize={imageSize} /> <p> <b>{place.name}</b> {': ' + place.description} </p> </> ); } function PlaceImage({ place, imageSize }) { return ( <img src={getImageUrl(place)} alt={place.name} width={imageSize} height={imageSize} /> ); }