v19.2Latest

Передача данных вглубь с помощью Context

Обычно вы передаете информацию от родительского компонента дочернему через пропсы. Но передача пропсов может стать многословной и неудобной, если вам приходится передавать их через множество промежуточных компонентов или если многим компонентам вашего приложения нужна одна и та же информация.Contextпозволяет родительскому компоненту сделать некоторую информацию доступной для любого компонента в дереве ниже него — независимо от глубины — без явной передачи через пропсы.

Вы узнаете
  • Что такое «пробуривание пропсов» (prop drilling)
  • Как заменить повторяющуюся передачу пропсов контекстом
  • Распространенные случаи использования контекста
  • Распространенные альтернативы контексту

Проблема с передачей пропсов

Передача пропсов— отличный способ явно передавать данные через дерево интерфейса к компонентам, которые их используют.

Но передача пропсов может стать многословной и неудобной, когда вам нужно передать какой-то пропс глубоко в дереве или если многим компонентам нужен один и тот же пропс. Ближайший общий предок может быть далеко от компонентов, которым нужны данные, иподнятие состояниятак высоко может привести к ситуации, называемой «пробуриванием пропсов».

Поднятие состояния

Диаграмма с деревом из трех компонентов. Родитель содержит пузырь, представляющий значение, выделенное фиолетовым. Значение передается вниз каждому из двух дочерних компонентов, оба выделены фиолетовым.Диаграмма с деревом из трех компонентов. Родитель содержит пузырь, представляющий значение, выделенное фиолетовым. Значение передается вниз каждому из двух дочерних компонентов, оба выделены фиолетовым.

Пробуривание пропсов

Диаграмма с деревом из десяти узлов, каждый узел имеет два или меньше дочерних элемента. Корневой узел содержит пузырь, представляющий значение, выделенное фиолетовым. Значение передается вниз через двух дочерних элементов, каждый из которых передает значение, но не содержит его. Левый дочерний элемент передает значение вниз двум своим дочерним элементам, которые оба выделены фиолетовым. Правый дочерний элемент корня передает значение одному из своих двух дочерних элементов — правому, который выделен фиолетовым. Этот дочерний элемент передал значение через своего единственного потомка, который передает его вниз обоим своим двум дочерним элементам, которые выделены фиолетовым.Диаграмма с деревом из десяти узлов, каждый узел имеет два или меньше дочерних элемента. Корневой узел содержит пузырь, представляющий значение, выделенное фиолетовым. Значение передается вниз через двух дочерних элементов, каждый из которых передает значение, но не содержит его. Левый дочерний элемент передает значение вниз двум своим дочерним элементам, которые оба выделены фиолетовым. Правый дочерний элемент корня передает значение одному из своих двух дочерних элементов — правому, который выделен фиолетовым. Этот дочерний элемент передал значение через своего единственного потомка, который передает его вниз обоим своим двум дочерним элементам, которые выделены фиолетовым.

Разве не было бы здорово, если бы существовал способ «телепортировать» данные к компонентам в дереве, которые в них нуждаются, без передачи пропсов? С помощью функции контекста в React это возможно!

Context: альтернатива передаче пропсов

Контекст позволяет родительскому компоненту предоставлять данные всему дереву ниже него. Контекст имеет множество применений. Вот один пример. Рассмотрим этот компонентHeading, который принимаетlevelдля определения своего размера:

Допустим, вы хотите, чтобы несколько заголовков внутри одной и той жеSectionвсегда имели одинаковый размер:

В настоящее время вы передаете пропсlevelкаждому<Heading>отдельно:

Было бы удобно, если бы вы могли передавать пропсlevelкомпоненту<Section>вместо этого и убрать его из<Heading>. Таким образом, вы могли бы гарантировать, что все заголовки в одном разделе имеют одинаковый размер:

Но как компонент<Heading>может узнать уровень своего ближайшего<Section>?Для этого потребуется какой-то способ, чтобы дочерний компонент мог «запросить» данные откуда-то сверху в дереве.

С помощью пропсов этого сделать нельзя. Здесь на помощь приходит контекст. Вы сделаете это в три шага:

  1. Создайтеконтекст. (Вы можете назвать егоLevelContext, так как он предназначен для уровня заголовка.)
  2. Используйтеэтот контекст в компоненте, которому нужны данные. (Headingбудет использоватьLevelContext.)
  3. Предоставьтеэтот контекст из компонента, который определяет данные. (Sectionбудет предоставлятьLevelContext.)

Контекст позволяет родительскому компоненту — даже отдаленному! — предоставлять некоторые данные всему дереву внутри него.

Использование контекста в ближайших дочерних компонентах

Диаграмма с деревом из трех компонентов. Родитель содержит пузырь, представляющий значение, выделенное оранжевым, которое проецируется вниз на двух дочерних компонентов, каждый из которых выделен оранжевым.Диаграмма с деревом из трех компонентов. Родитель содержит пузырь, представляющий значение, выделенное оранжевым, которое проецируется вниз на двух дочерних компонентов, каждый из которых выделен оранжевым.

Использование контекста в отдаленных дочерних компонентах

Диаграмма с деревом из десяти узлов, каждый узел имеет два дочерних узла или меньше. Корневой родительский узел содержит пузырь, представляющий значение, выделенное оранжевым. Значение проецируется вниз непосредственно на четыре листовых узла и один промежуточный компонент в дереве, все они выделены оранжевым. Ни один из других промежуточных компонентов не выделен.Диаграмма с деревом из десяти узлов, каждый узел имеет два дочерних узла или меньше. Корневой родительский узел содержит пузырь, представляющий значение, выделенное оранжевым. Значение проецируется вниз непосредственно на четыре листовых узла и один промежуточный компонент в дереве, все они выделены оранжевым. Ни один из других промежуточных компонентов не выделен.

Шаг 1: Создание контекста

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

Единственный аргумент дляcreateContext— этозначение по умолчанию. Здесь1означает самый крупный уровень заголовка, но вы можете передать любое значение (даже объект). Значение по умолчанию станет значимым на следующем шаге.

Шаг 2: Использование контекста

Импортируйте хукuseContextиз React и ваш контекст:

В данный момент компонентHeadingчитаетlevelиз пропсов:

Вместо этого удалите пропlevelи прочитайте значение из только что импортированного контекстаLevelContext:

useContext— это хук. Как иuseState с useReducer, вызывать хук можно только непосредственно внутри React-компонента (не внутри циклов или условий).useContextсообщает React, что компонентHeadingхочет прочитатьLevelContext.

Теперь, когда у компонентаHeadingнет пропаlevel, вам больше не нужно передавать проп level вHeadingв JSX вот так:

Обновите JSX так, чтобы его получалSection:

Напомним, вот разметка, которую вы пытались заставить работать:

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

Если вы не предоставите контекст, React будет использовать значение по умолчанию, которое вы указали на предыдущем шаге. В этом примере вы указали1в качестве аргумента дляcreateContext, поэтомуuseContext(LevelContext)возвращает1, устанавливая все эти заголовки как<h1>. Давайте исправим эту проблему, заставив каждыйSectionпредоставлять свой собственный контекст.

Шаг 3: Предоставьте контекст

КомпонентSectionв данный момент рендерит своих дочерних элементов:

Оберните их провайдером контекста, чтобы предоставить имLevelContext:

Это говорит React: «если какой-либо компонент внутри этого<Section>запрашиваетLevelContext, дайте им этотlevel». Компонент будет использовать значение ближайшего<LevelContext>в дереве пользовательского интерфейса над ним.

Это тот же результат, что и в исходном коде, но вам не нужно передавать пропсlevelкаждому компонентуHeading! Вместо этого он «выясняет» свой уровень заголовка, спрашивая ближайшийSectionвыше:

  1. Вы передаете пропсlevelкомпоненту<Section>.
  2. Sectionоборачивает своих потомков в<LevelContext value={level}>.
  3. Headingзапрашивает ближайшее значениеLevelContextвыше с помощьюuseContext(LevelContext).

Использование и предоставление контекста из одного компонента

В настоящее время вам все еще приходится вручную указыватьlevelдля каждого раздела:

Поскольку контекст позволяет читать информацию из компонента выше, каждыйSectionмог бы читатьlevel из Sectionвыше и автоматически передаватьlevel + 1вниз. Вот как это можно сделать:

С этим изменением вам больше не нужно передавать пропсlevel ни в <Section>, ни в<Heading>:

Теперь иHeading, иSectionчитаютLevelContext, чтобы определить, насколько они «глубоко» находятся. АSectionоборачивает своих потомков вLevelContext, чтобы указать, что все внутри него находится на более «глубоком» уровне.

Примечание

В этом примере используются уровни заголовков, потому что они наглядно показывают, как вложенные компоненты могут переопределять контекст. Но контекст полезен и для многих других случаев. Вы можете передавать вниз любую информацию, необходимую всему поддереву: текущую цветовую тему, текущего авторизованного пользователя и так далее.

Контекст проходит через промежуточные компоненты

Вы можете вставить сколько угодно компонентов между компонентом, который предоставляет контекст, и тем, который его использует. Это включает как встроенные компоненты, такие как<div>, так и компоненты, которые вы можете создать сами.

В этом примере один и тот же компонентPost(с пунктирной рамкой) рендерится на двух разных уровнях вложенности. Обратите внимание, что<Heading>внутри него автоматически получает свой уровень от ближайшего<Section>:

Для того чтобы это работало, вам не нужно было делать ничего особенного. КомпонентSectionзадаёт контекст для дерева внутри себя, поэтому вы можете вставить<Heading>где угодно, и он получит правильный размер. Попробуйте это в песочнице выше!

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

Принцип работы контекста может напомнить вам онаследовании CSS-свойств.В CSS вы можете указатьcolor: blueдля<div>, и любой DOM-узел внутри него, независимо от глубины, унаследует этот цвет, если какой-то другой DOM-узел посередине не переопределит его с помощьюcolor: green. Аналогично, в React единственный способ переопределить контекст, приходящий сверху, — это обернуть дочерние элементы в провайдер контекста с другим значением.

В CSS разные свойства, такие какcolor и background-color, не переопределяют друг друга. Вы можете установить<div>значениеcolorв красный, не влияя наbackground-color. Аналогично,разные контексты React не переопределяют друг друга.Каждый контекст, созданный с помощьюcreateContext(), полностью отделён от других и связывает компоненты, использующие и предоставляющиеименно этотконтекст. Один компонент может использовать или предоставлять множество различных контекстов без проблем.

Прежде чем использовать контекст

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

Вот несколько альтернатив, которые стоит рассмотреть перед использованием контекста:

  1. Начните спередачи пропсов.Если ваши компоненты нетривиальны, нередко приходится передавать десяток пропсов через десяток компонентов. Это может казаться рутиной, но это делает очень понятным, какие компоненты используют какие данные! Человек, поддерживающий ваш код, будет благодарен, что вы сделали поток данных явным с помощью пропсов.
  2. Извлекайте компоненты ипередавайте JSX как childrenим.Если вы передаёте некоторые данные через множество слоёв промежуточных компонентов, которые не используют эти данные (а только передают их дальше), это часто означает, что вы забыли извлечь некоторые компоненты по пути. Например, возможно, вы передаёте пропсы данных, такие какposts, в визуальные компоненты, которые не используют их напрямую, например<Layout posts={posts} />. Вместо этого сделайте так, чтобыLayoutпринималchildrenв качестве пропса, и рендерите<Layout><Posts posts={posts} /></Layout>. Это уменьшает количество слоёв между компонентом, определяющим данные, и тем, который в них нуждается.

Если ни один из этих подходов вам не подходит, рассмотрите контекст.

Случаи использования контекста

  • Тематизация:Если ваше приложение позволяет пользователю менять его внешний вид (например, тёмный режим), вы можете разместить провайдер контекста в верхней части приложения и использовать этот контекст в компонентах, которым нужно настроить свой визуальный вид.
  • Текущий аккаунт:Многим компонентам может потребоваться знать текущего вошедшего пользователя. Размещение этой информации в контексте делает её удобной для чтения в любом месте дерева. Некоторые приложения также позволяют работать с несколькими аккаунтами одновременно (например, чтобы оставить комментарий от другого пользователя). В таких случаях может быть удобно обернуть часть интерфейса во вложенный провайдер с другим значением текущего аккаунта.
  • Маршрутизация:Большинство решений для маршрутизации используют контекст внутри себя для хранения текущего маршрута. Именно так каждая ссылка «знает», активна она или нет. Если вы создаёте собственный маршрутизатор, вам, вероятно, тоже захочется это сделать.
  • Управление состоянием:По мере роста вашего приложения у вас может оказаться много состояния ближе к верхней части приложения. Многие удалённые компоненты ниже могут захотеть его изменить. Обычноиспользуют редюсер вместе с контекстомдля управления сложным состоянием и передачи его вниз удалённым компонентам без лишних хлопот.

Контекст не ограничивается статическими значениями. Если вы передадите другое значение при следующем рендере, React обновит все компоненты ниже, которые его читают! Вот почему контекст часто используется в сочетании с состоянием.

В общем случае, если какая-то информация нужна удалённым компонентам в разных частях дерева, это хороший признак того, что контекст вам поможет.

Резюме

  • Контекст позволяет компоненту предоставлять некоторую информацию всему дереву ниже него.
  • Чтобы передать контекст:
    1. Создайте и экспортируйте его с помощьюexport const MyContext = createContext(defaultValue).
    2. Передайте его в хукuseContext(MyContext)для чтения в любом дочернем компоненте, независимо от глубины.
    3. Оберните дочерние элементы в<MyContext value={...}>, чтобы предоставить его из родительского компонента.
  • Контекст проходит через любые промежуточные компоненты.
  • Контекст позволяет писать компоненты, которые «адаптируются к своему окружению».
  • Прежде чем использовать контекст, попробуйте передавать пропсы или передавать JSX какchildren.

Попробуйте выполнить несколько заданий

Challenge 1 of 1:Замените проброс пропсов контекстом #

В этом примере переключение флажка изменяет проп imageSize, передаваемый каждому компоненту <PlaceImage>. Состояние флажка хранится в компоненте верхнего уровня App, но каждый <PlaceImage> должен знать о нём.

В настоящее время App передаёт imageSize в List, который передаёт его каждому Place, который передаёт его в PlaceImage. Удалите проп imageSize и вместо этого передавайте его из компонента App напрямую в PlaceImage.

Вы можете объявить контекст в Context.js.