Передача данных на глубину с помощью контекста
Обычно вы передаете информацию от родительского компонента до дочернего компонента через атрибуты. Но передача атрибутов может стать громоздкой и неудобной, если вам нужно передать их через много компонентов посередине, или если многим компонентам вашего приложения нужна одна и та же информация. Контекст позволяет родительскому компоненту сделать некоторую информацию доступной любому компоненту в дереве ниже него — независимо от того, насколько глубоко он расположен — без явной передачи через атрибуты.
Вы узнаете
- Что такое «проход атрибутов»
- Как заменить повторяющуюся передачу атрибутов с помощью контекста
- Общие случаи использования контекста
- Общие альтернативы контексту
Проблема с передачей атрибутов
Передача атрибутов — отличный способ явно передавать данные по дереву пользовательского интерфейса в компоненты, которые их используют.
Но передача атрибутов может стать громоздкой и неудобной, когда вам нужно передать некоторый атрибут глубоко по дереву, или если многим компонентам нужен один и тот же атрибут. Ближайший общий предок может находиться далеко от компонентов, которым нужны данные, и поднятие состояния так высоко может привести к ситуации, называемой «проход атрибутов».
Поднятие состояния вверх
Проход атрибутов
Разве не было бы здорово, если бы существовал способ «телепортировать» данные в компоненты дерева, которые их нуждаются, без передачи атрибутов? С помощью функции контекста React это возможно!
Контекст: альтернатива передаче атрибутов
Контекст позволяет родительскому компоненту предоставить данные всему дереву ниже него. Существует множество применений контекста. Вот один пример. Рассмотрим этот Heading компонент, который принимает level для своего размера:
import Heading from './Heading.js'; import Section from './Section.js'; export default function Page() { return ( <Section> <Heading level={1}>Title</Heading> <Heading level={2}>Heading</Heading> <Heading level={3}>Sub-heading</Heading> <Heading level={4}>Sub-sub-heading</Heading> <Heading level={5}>Sub-sub-sub-heading</Heading> <Heading level={6}>Sub-sub-sub-sub-heading</Heading> </Section> ); }
Предположим, вы хотите, чтобы несколько заголовков в одном и том же Section всегда имели одинаковый размер:
import Heading from './Heading.js'; import Section from './Section.js'; export default function Page() { return ( <Section> <Heading level={1}>Title</Heading> <Section> <Heading level={2}>Heading</Heading> <Heading level={2}>Heading</Heading> <Heading level={2}>Heading</Heading> <Section> <Heading level={3}>Sub-heading</Heading> <Heading level={3}>Sub-heading</Heading> <Heading level={3}>Sub-heading</Heading> <Section> <Heading level={4}>Sub-sub-heading</Heading> <Heading level={4}>Sub-sub-heading</Heading> <Heading level={4}>Sub-sub-heading</Heading> </Section> </Section> </Section> </Section> ); }
В настоящее время вы передаете атрибут level в каждый <Heading> по отдельности:
<Section>
<Heading level={3}>About</Heading>
<Heading level={3}>Photos</Heading>
<Heading level={3}>Videos</Heading>
</Section> Было бы неплохо передать атрибут level компоненту <Section> вместо этого и убрать его из <Heading>. Таким образом, вы могли бы обеспечить, что все заголовки в одном разделе имеют одинаковый размер:
<Section level={3}>
<Heading>About</Heading>
<Heading>Photos</Heading>
<Heading>Videos</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}>Sub-sub-heading</Heading>
<Heading level={4}>Sub-sub-heading</Heading>
<Heading level={4}>Sub-sub-heading</Heading>
</Section> Обновите JSX, чтобы он получал его вместо этого:
<Section level={4}>
<Heading>Sub-sub-heading</Heading>
<Heading>Sub-sub-heading</Heading>
<Heading>Sub-sub-heading</Heading>
</Section> Напоминание: это разметка, которую вы пытались сделать:
import Heading from './Heading.js'; import Section from './Section.js'; export default function Page() { return ( <Section level={1}> <Heading>Title</Heading> <Section level={2}> <Heading>Heading</Heading> <Heading>Heading</Heading> <Heading>Heading</Heading> <Section level={3}> <Heading>Sub-heading</Heading> <Heading>Sub-heading</Heading> <Heading>Sub-heading</Heading> <Section level={4}> <Heading>Sub-sub-heading</Heading> <Heading>Sub-sub-heading</Heading> <Heading>Sub-sub-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, дайте им этот level». Компонент будет использовать значение ближайшего <LevelContext.Provider> в дереве пользовательского интерфейса над ним.
import Heading from './Heading.js'; import Section from './Section.js'; export default function Page() { return ( <Section level={1}> <Heading>Title</Heading> <Section level={2}> <Heading>Heading</Heading> <Heading>Heading</Heading> <Heading>Heading</Heading> <Section level={3}> <Heading>Sub-heading</Heading> <Heading>Sub-heading</Heading> <Heading>Sub-heading</Heading> <Section level={4}> <Heading>Sub-sub-heading</Heading> <Heading>Sub-sub-heading</Heading> <Heading>Sub-sub-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>Title</Heading> <Section> <Heading>Heading</Heading> <Heading>Heading</Heading> <Heading>Heading</Heading> <Section> <Heading>Sub-heading</Heading> <Heading>Sub-heading</Heading> <Heading>Sub-heading</Heading> <Section> <Heading>Sub-sub-heading</Heading> <Heading>Sub-sub-heading</Heading> <Heading>Sub-sub-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>My Profile</Heading> <Post title="Hello traveller!" body="Read about my adventures." /> <AllPosts /> </Section> ); } function AllPosts() { return ( <Section> <Heading>Posts</Heading> <RecentPosts /> </Section> ); } function RecentPosts() { return ( <Section> <Heading>Recent Posts</Heading> <Post title="Flavors of Lisbon" body="...those pastéis de nata!" /> <Post title="Buenos Aires in the rhythm of tango" body="I loved it!" /> </Section> ); } function Post({ title, body }) { return ( <Section isFancy={true}> <Heading> {title} </Heading> <p><i>{body}</i></p> </Section> ); }
Вы ничего не делали специально для того, чтобы это работало. Section задаёт контекст для дерева внутри него, поэтому вы можете вставить <Heading> в любое место, и он будет иметь правильный размер. Попробуйте это в песочнице выше!
Контекст позволяет вам создавать компоненты, которые «адаптируются к своему окружению» и отображают себя по-разному в зависимости от того, где (или, другими словами, в каком контексте) они отображаются.
Возможно, способ работы контекста напомнит вам о наследовании свойств CSS. В CSS вы можете указать color: blue для <div>, и любой узел DOM внутри него, независимо от того, насколько он глубоко вложен, унаследует этот цвет, если какой-то другой узел DOM посередине не переопределит его с помощью color: green. Аналогично, в React единственный способ переопределить какой-либо контекст, полученный сверху, — это обернуть потомков в поставщик контекста с другим значением.
В CSS различные свойства, такие как color и background-color не переопределяют друг друга. Вы можете установить <div> color всех <div> в красный цвет, не влияя на background-color. Аналогично, разные контексты React не переопределяют друг друга. Каждый контекст, который вы создаёте с помощью createContext() полностью отделён от других и связывает компоненты с использованием и предоставлением этого конкретного контекста. Один компонент может использовать или предоставлять множество различных контекстов без проблем.
Прежде чем использовать контекст
Контекст очень соблазнителен для использования! Однако это также означает, что слишком легко его переиспользовать. Просто потому, что вам нужно передать некоторые атрибуты на несколько уровней вниз, не означает, что вы должны помещать эту информацию в контекст.
Вот несколько альтернатив, которые следует рассмотреть перед использованием контекста:
- Начните с передачи свойств. Если ваши компоненты не тривиальны, передача дюжины свойств через дюжину компонентов — обычное дело. Возможно, это покажется сложным, но это очень четко показывает, какие компоненты используют какие данные! Человек, который будет поддерживать ваш код, будет рад, что вы сделали поток данных с помощью свойств явным.
-
Извлеките компоненты и передайте JSX в качестве
children. Если вы передаете некоторые данные через много слоев промежуточных компонентов, которые не используют эти данные (а только передают их дальше), это часто означает, что вы забыли извлечь некоторые компоненты по пути. Например, может быть, вы передаете данные-свойства, такие какpostsвизуальным компонентам, которые не используют их напрямую, таким как<Layout posts={posts} />. Вместо этого сделайтеLayoutприниматьchildrenв качестве свойства и отображать<Layout><Posts posts={posts} /></Layout>. Это уменьшает количество слоев между компонентом, определяющим данные, и компонентом, которому они нужны.
Если ни один из этих методов не подходит для вас, рассмотрите контекст.
Примеры использования контекста
- Тема: Если ваше приложение позволяет пользователю изменять его внешний вид (например, темный режим), вы можете поместить поставщик контекста в верхней части приложения и использовать этот контекст в компонентах, которым нужно настроить свой визуальный вид.
- Текущий аккаунт: Многие компоненты могут нуждаться в информации о текущем вошедшем пользователе. Размещение этой информации в контексте делает ее удобной для чтения в любом месте дерева компонентов. Некоторые приложения также позволяют работать с несколькими учетными записями одновременно (например, для оставления комментария как другого пользователя). В таких случаях удобно обернуть часть пользовательского интерфейса во вложенный поставщик с другим значением текущего аккаунта.
- Маршрутизация: Большинство решений по маршрутизации используют контекст внутри для хранения текущего маршрута. Именно поэтому каждая ссылка «знает», активна она или нет. Если вы создаете свой маршрутизатор, вы можете сделать то же самое.
- Управление состоянием: По мере роста приложения вы можете получить множество состояний, расположенных ближе к верху приложения. Многие удаленные компоненты ниже могут захотеть его изменить. Принято использовать редьюсер совместно с контекстом для управления сложным состоянием и передавать его удаленным компонентам без особых затруднений.
Контекст не ограничивается статическими значениями. Если вы передаете другое значение при следующем рендеринге, React обновит все компоненты, считывающие его ниже! Именно поэтому контекст часто используется в сочетании с состоянием.
В целом, если некоторая информация нужна удаленным компонентам в разных частях дерева, это хороший признак того, что контекст вам поможет.
Резюме
- Контекст позволяет компоненту предоставлять некоторую информацию всему дереву под ним.
- Для передачи контекста:
- Создайте и экспортируйте его с помощью
export const MyContext = createContext(defaultValue). - Передайте его в
useContext(MyContext)хук, чтобы читать его в любом дочернем компоненте, независимо от глубины. - Окружите дочерние компоненты
<MyContext.Provider value={...}>для передачи его от родительского компонента.
- Создайте и экспортируйте его с помощью
- Контекст передается через все компоненты посередине.
- Контекст позволяет вам создавать компоненты, которые «адаптируются к своему окружению».
- Перед использованием контекста попробуйте передавать свойства или передавать JSX в качестве
children.
Попробуйте выполнить некоторые задачи
Задача 1 из 1:
Замена проброса свойств контекстом
В этом примере переключение флажка изменяет imageSize свойство, передаваемое каждому <PlaceImage>. Состояние флажка хранится в 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); }} /> Use large images </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} /> ); }
© 2013–present Facebook Inc.
Licensed under the Creative Commons Attribution 4.0 International Public License.
https://18.react.dev/learn/passing-data-deeply-with-context