Глубокая передача данных с помощью контекста¶
Обычно вы передаете информацию от родительского компонента к дочернему компоненту с помощью реквизитов. Но передача реквизитов может стать многословной и неудобной, если вам приходится передавать их через множество компонентов в середине, или если многим компонентам в вашем приложении нужна одна и та же информация. Context позволяет родительскому компоненту сделать некоторую информацию доступной для любого компонента в дереве под ним - независимо от глубины - без явной передачи ее через props.
- Что такое "бурение реквизитов"
- Как заменить повторяющуюся передачу реквизитов контекстом
- Общие случаи использования контекста
- Общие альтернативы контексту
Проблема с передачей реквизитов {/the-problem-with-passing-props/}¶
Передача реквизитов - это отличный способ явной передачи данных через дерево вашего пользовательского интерфейса компонентам, которые их используют.
Но передача реквизитов может стать многословной и неудобной, если вам нужно передать какой-то реквизит глубоко в дереве, или если многим компонентам нужен один и тот же реквизит. Ближайший общий предок может быть далеко от компонентов, которым нужны данные, а поднятие состояния вверх так высоко может привести к ситуации, называемой "prop drilling".
\<ДиаграммаГруппа>
\<Diagram name="passing_data_lifting_state" height={160} width={608} captionPosition="top" alt="Диаграмма с деревом из трех компонентов. Родительский компонент содержит пузырек, представляющий значение, выделенное фиолетовым цветом. Значение перетекает вниз к каждому из двух дочерних компонентов, оба выделены фиолетовым цветом." >
Подъем состояния вверх
\</Diagram> \<Diagram name="passing_data_prop_drilling" height={430} width={608} captionPosition="top" alt="Диаграмма с деревом из десяти узлов, каждый узел с двумя детьми или меньше. Корневой узел содержит пузырек, представляющий значение, выделенное фиолетовым цветом. Значение проходит вниз через два дочерних узла, каждый из которых передает значение, но не содержит его. Левый ребенок передает значение вниз двум детям, которые оба выделены фиолетовым цветом. Правый ребенок корня передает значение одному из двух своих детей - правому, который выделен фиолетовым цветом. Этот ребенок передает значение через своего единственного ребенка, который передает его обоим своим детям, выделенным фиолетовым цветом.">
Бурение реквизита
\</Diagram>
\</DiagramGroup>
Было бы здорово, если бы существовал способ "телепортировать" данные к тем компонентам в дереве, которым они нужны, без передачи реквизитов. С функцией контекста React это возможно!
Контекст: альтернатива передаче props {/context-an-alternative-to-passing-props/}¶
Контекст позволяет родительскому компоненту предоставлять данные всему дереву под ним. Существует множество вариантов использования контекста. Вот один из примеров. Рассмотрим компонент Heading
, который принимает level
для своего размера:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
|
1 2 3 4 5 |
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
|
1 2 3 4 5 6 |
|
Допустим, вы хотите, чтобы несколько заголовков в одном разделе
всегда имели одинаковый размер:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
|
1 2 3 4 5 |
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
|
1 2 3 4 5 6 |
|
В настоящее время вы передаете параметр level
каждому <Heading>
отдельно:
1 2 3 4 5 |
|
Было бы неплохо, если бы вы могли передавать параметр level
компоненту <Section>
и удалять его из <Heading>
. Таким образом можно было бы добиться того, чтобы все заголовки в одном разделе имели одинаковый размер:
1 2 3 4 5 |
|
Но как компонент <Глава>
может узнать уровень ближайшего к нему <Раздела>
? *Для этого нужно, чтобы дочерний компонент мог "запрашивать" данные откуда-то сверху в дереве.
Вы не можете сделать это только с помощью реквизитов. Здесь в игру вступает контекст. Вы сделаете это в три шага:
- Создайте контекст. (Вы можете назвать его
LevelContext
, поскольку он предназначен для уровня заголовка). - Используем этот контекст из компонента, которому нужны данные. (
Heading
будет использоватьLevelContext
). - Предоставить этот контекст от компонента, который определяет данные. (
Section
будет предоставлятьLevelContext
).
Контекст позволяет родителю - даже очень далекому - предоставлять некоторые данные всему дереву внутри него.
\<ДиаграммаГруппа>
\<Diagram name="passing_data_context_close" height={160} width={608} captionPosition="top" alt="Диаграмма с деревом из трех компонентов. Родитель содержит пузырек, представляющий значение, выделенное оранжевым цветом, которое проецируется вниз на два дочерних компонента, каждый из которых выделен оранжевым цветом." >
Использование контекста в близких дочерних компонентах
\<!--Диаграмма-->
\<Diagram name="passing_data_context_far" height={430} width={608} captionPosition="top" alt="Диаграмма с деревом из десяти узлов, каждый узел с двумя детьми или меньше. Корневой родительский узел содержит пузырек, представляющий значение, выделенное оранжевым цветом. Значение проецируется вниз непосредственно на четыре листа и один промежуточный компонент дерева, которые все выделены оранжевым цветом. Ни один из других промежуточных компонентов не выделен.">.
Использование контекста в далеких детях
\</Diagram>
\</DiagramGroup>
Шаг 1: Создание контекста {/step-1-create-the-context/}¶
Во-первых, вам нужно создать контекст. Вам нужно будет экспортировать его из файла, чтобы ваши компоненты могли его использовать:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
|
1 2 3 4 5 |
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
|
1 2 3 |
|
1 2 3 4 5 6 |
|
Единственным аргументом для createContext
является значение по умолчанию. Здесь 1
означает самый большой уровень заголовка, но вы можете передать любое значение (даже объект). Значение значения по умолчанию вы увидите в следующем шаге.
Шаг 2: Использование контекста {/step-2-use-the-context/}¶
Импортируйте хук useContext
из React и ваш контекст:
1 2 |
|
В настоящее время компонент Heading
считывает level
из реквизита:
1 2 3 |
|
Вместо этого удалите параметр level
и считайте значение из только что импортированного контекста LevelContext
:
1 2 3 4 |
|
useContext
- это хук. Как и useState
и useReducer
, хук можно вызывать только непосредственно внутри компонента React (не внутри циклов или условий). useContext
сообщает React, что компонент Heading
хочет прочитать LevelContext
..
Теперь, когда у компонента Heading
нет свойства level
, вам больше не нужно передавать свойство level компоненту Heading
в вашем JSX таким образом:
1 2 3 4 5 |
|
Обновите JSX так, чтобы вместо него его получала Section
:
1 2 3 4 5 |
|
Напомним, что это разметка, которую вы пытались заставить работать:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
|
1 2 3 4 5 |
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
|
1 2 3 |
|
1 2 3 4 5 6 |
|
Заметьте, что этот пример не совсем работает! Все заголовки имеют одинаковый размер, потому что хотя вы используете контекст, вы еще не предоставили его. React не знает, где его взять!
Если вы не предоставите контекст, React будет использовать значение по умолчанию, которое вы указали в предыдущем шаге. В этом примере вы указали 1
в качестве аргумента для createContext
, поэтому useContext(LevelContext)
возвращает 1
, устанавливая все эти заголовки в <h1>
. Давайте решим эту проблему, предоставив каждой Section
свой собственный контекст.
Шаг 3: Предоставление контекста {/step-3-provide-the-context/}¶
Компонент Section
в настоящее время отображает свои дочерние элементы:
1 2 3 4 5 |
|
Оберните их провайдером контекста, чтобы предоставить им LevelContext
:
1 2 3 4 5 6 7 8 9 10 11 |
|
Это говорит React: "если какой-либо компонент внутри этой <Section>
запрашивает LevelContext
, дайте ему этот level
". Компонент будет использовать значение ближайшего <LevelContext.Provider>
в дереве пользовательского интерфейса над ним.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
|
1 2 3 4 5 6 7 8 9 10 11 |
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
|
1 2 3 |
|
1 2 3 4 5 6 |
|
Это тот же результат, что и в оригинальном коде, но вам не нужно передавать свойство level
каждому компоненту Heading
! Вместо этого, он "вычисляет" уровень заголовка, спрашивая ближайший Section
выше:
- Вы передаете реквизит
level
компоненту<Section>
. Section
оборачивает свои дочерние элементы в<LevelContext.Provider value={level}>
.Heading
запрашивает ближайшее значениеLevelContext
выше с помощьюuseContext(LevelContext)
.
Использование и предоставление контекста из одного и того же компонента {/using-and-providing-context-from-the-same-component/}¶
В настоящее время вы все еще должны указывать уровень
каждой секции вручную:
1 2 3 4 5 6 7 8 |
|
Поскольку контекст позволяет вам читать информацию из компонента выше, каждый Section
мог бы читать level
из Section
выше, и передавать level + 1
вниз автоматически. Вот как это можно сделать:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
|
С этим изменением вам не нужно передавать параметр level
либо в <Section>
, либо в <Heading>
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
|
1 2 3 4 5 6 7 8 9 10 11 12 13 |
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
|
1 2 3 |
|
1 2 3 4 5 6 |
|
Now both Heading
and Section
read the LevelContext
to figure out how “deep” they are. And the Section
wraps its children into the LevelContext
to specify that anything inside of it is at a “deeper” level.
This example uses heading levels because they show visually how nested components can override context. But context is useful for many other use cases too. You can pass down any information needed by the entire subtree: the current color theme, the currently logged in user, and so on.
Context passes through intermediate components {/context-passes-through-intermediate-components/}¶
You can insert as many components as you like between the component that provides context and the one that uses it. This includes both built-in components like <div>
and components you might build yourself.
In this example, the same Post
component (with a dashed border) is rendered at two different nesting levels. Notice that the <Heading>
inside of it gets its level automatically from the closest <Section>
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 |
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
|
1 2 3 |
|
1 2 3 4 5 6 7 8 9 10 |
|
You didn’t do anything special for this to work. A Section
specifies the context for the tree inside it, so you can insert a <Heading>
anywhere, and it will have the correct size. Try it in the sandbox above!
Context lets you write components that “adapt to their surroundings” and display themselves differently depending on where (or, in other words, in which context) they are being rendered.
How context works might remind you of CSS property inheritance. In CSS, you can specify color: blue
for a <div>
, and any DOM node inside of it, no matter how deep, will inherit that color unless some other DOM node in the middle overrides it with color: green
. Similarly, in React, the only way to override some context coming from above is to wrap children into a context provider with a different value.
In CSS, different properties like color
and background-color
don’t override each other. You can set all <div>
’s color
to red without impacting background-color
. Similarly, different React contexts don’t override each other. Each context that you make with createContext()
is completely separate from other ones, and ties together components using and providing that particular context. One component may use or provide many different contexts without a problem.
Прежде чем использовать контекст {/before-you-use-context/}¶
Контекст очень заманчиво использовать! Однако, это также означает, что его слишком легко использовать чрезмерно. Если вам нужно передать некоторые реквизиты на несколько уровней вглубь, это не значит, что вы должны поместить эту информацию в контекст.
Вот несколько альтернатив, которые вы должны рассмотреть, прежде чем использовать контекст:
- Начните с передачи реквизитов Если ваши компоненты не тривиальны, нет ничего необычного в том, чтобы передать дюжину реквизитов вниз через дюжину компонентов. Это может показаться трудоемкой задачей, но так становится ясно, какие компоненты используют те или иные данные! Человек, обслуживающий ваш код, будет рад, что вы сделали поток данных явным с помощью реквизитов.
- Извлекайте компоненты и передавайте JSX как
дети
к ним. Если вы передаете некоторые данные через множество уровней промежуточных компонентов, которые не используют эти данные (и передают их только дальше вниз), это часто означает, что вы забыли извлечь некоторые компоненты по пути. Например, вы передаете реквизиты данных, такие какposts
, визуальным компонентам, которые не используют их напрямую, например<Layout posts={posts} />
. Вместо этого, заставьтеLayout
приниматьchildren
в качестве реквизита и выводить<Layout><Posts posts={posts} /></Layout>
. Это уменьшает количество уровней между компонентом, задающим данные, и компонентом, которому они нужны.
Если ни один из этих подходов вам не подходит, подумайте о контексте.
Примеры использования контекста {/use-cases-for-context/}¶
- Тематика: Если ваше приложение позволяет пользователю изменять его внешний вид (например, темный режим), вы можете поместить провайдер контекста в верхней части вашего приложения,
- Текущая учетная запись: Многим компонентам может понадобиться знать текущего пользователя, вошедшего в систему. Если поместить его в контекст, его будет удобно прочитать в любом месте дерева. Некоторые приложения также позволяют работать с несколькими учетными записями одновременно (например, оставлять комментарии под другим пользователем). В этих случаях может быть удобно обернуть часть пользовательского интерфейса во вложенный провайдер с другим значением текущего счета.
- Маршрутизация: Большинство решений для маршрутизации используют внутренний контекст для хранения текущего маршрута. Так каждая ссылка "знает", активна она или нет. Если вы создаете свой собственный маршрутизатор, вы, возможно, захотите сделать это тоже.
- Управление состоянием: По мере роста вашего приложения, вы можете оказаться с большим количеством состояния ближе к вершине приложения. Многие удаленные компоненты внизу могут захотеть его изменить. Обычно используется reducer вместе с контекстом для управления сложным состоянием и передачи его вниз к удаленным компонентам без лишних проблем.
Контекст не ограничивается статическими значениями. Если при следующем рендере вы передадите другое значение, React обновит все компоненты, читающие его ниже! Вот почему контекст часто используется в сочетании с состоянием.
В общем, если какая-то информация нужна удаленным компонентам в разных частях дерева, это верный признак того, что контекст вам поможет.
\<Recap>
- Контекст позволяет компоненту предоставлять некоторую информацию всему дереву под ним.
- Чтобы передать контекст:
- Создайте и экспортируйте его с помощью
export const MyContext = createContext(defaultValue)
. - Передайте его в хук
useContext(MyContext)
, чтобы прочитать его в любом дочернем компоненте, независимо от его глубины. - Заверните дочерние компоненты в
<MyContext.Provider value={...}>
, чтобы предоставить его от родителя.
- Создайте и экспортируйте его с помощью
- Контекст проходит через любые компоненты в середине.
- Контекст позволяет вам писать компоненты, которые "адаптируются к своему окружению".
- Прежде чем использовать контекст, попробуйте передать props или передать JSX в качестве
дочерних
.
\</Recap>
\<Проблемы>
Замените сверление реквизита контекстом {/replace-prop-drilling-with-context/}¶
В этом примере переключение флажка изменяет параметр imageSize
, передаваемый каждому <PlaceImage>
. Состояние флажка хранится в компоненте верхнего уровня App
, но каждый <PlaceImage>
должен знать об этом.
В настоящее время App
передает imageSize
в List
, который передает его в каждое Place
, которое передает его в PlaceImage
. Удалите реквизит imageSize
, и вместо этого передавайте его из компонента App
непосредственно в PlaceImage
.
Вы можете объявить контекст в файле Context.js
.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 |
|
1 |
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 |
|
1 2 3 |
|
1 2 3 4 5 6 7 8 9 10 11 |
|
\<Решение>
Удалите свойство imageSize
из всех компонентов.
Создайте и экспортируйте ImageSizeContext
из Context.js
. Затем оберните список в <ImageSizeContext.Provider value={imageSize}>
, чтобы передать значение вниз, и useContext(ImageSizeContext)
, чтобы прочитать его в PlaceImage
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 |
|
1 2 3 |
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 |
|
1 2 3 |
|
1 2 3 4 5 6 7 8 9 10 11 |
|
Обратите внимание, что компонентам в середине больше не нужно передавать imageSize
.
\</Solution>
\</Challenges>