Глубокая передача данных с помощью контекста¶
Обычно вы передаете информацию от родительского компонента к дочернему компоненту с помощью пропсов. Но передача пропсов может стать многословной и неудобной, если вам приходится передавать их через множество компонентов в середине, или если многим компонентам в вашем приложении нужна одна и та же информация. Контекст позволяет родительскому компоненту сделать некоторую информацию доступной для любого компонента в дереве под ним - независимо от глубины - без явной передачи ее через параметры.
Вы узнаете
- Что такое "проброс пропсов"
- Как заменить повторяющуюся передачу пропсов контекстом
- Общие случаи использования контекста
- Общие альтернативы контексту
Проблема с передачей пропсов¶
Передача пропсов - это отличный способ явной передачи данных через дерево вашего пользовательского интерфейса компонентам, которые их используют.
Но передача пропсов может стать многословной и неудобной, если вам нужно передать какой-то пропс глубоко в дереве, или если многим компонентам нужен один и тот же пропс. Ближайший общий предок может быть далеко от компонентов, которым нужны данные, а поднятие состояния вверх так высоко может привести к ситуации, называемой "prop drilling".
Подъем состояния вверх
Проброс пропса
Было бы здорово, если бы существовал способ "телепортировать" данные к тем компонентам в дереве, которым они нужны, без передачи пропсов. С функцией контекста React это возможно!
Контекст: альтернатива передаче 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 |
|
Допустим, вы хотите, чтобы несколько заголовков в одном 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 |
|
1 2 3 4 5 |
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
|
В настоящее время вы передаете параметр level
каждому <Heading>
отдельно:
1 2 3 4 5 |
|
Было бы неплохо, если бы вы могли передавать параметр level
компоненту <Section>
и удалять его из <Heading>
. Таким образом можно было бы добиться того, чтобы все заголовки в одном разделе имели одинаковый размер:
1 2 3 4 5 |
|
Но как компонент <Heading>
может узнать параметр level
ближайшего к нему <Section>
? Для этого нужно, чтобы дочерний компонент мог "запрашивать" данные откуда-то сверху в дереве.
Вы не можете сделать это только с помощью пропсов. Здесь в игру вступает контекст. Вы сделаете это в три шага:
- Создайте контекст. (Вы можете назвать его
LevelContext
, поскольку он предназначен для параметрlevel
вHeading
). - Используем этот контекст из компонента, которому нужны данные. (
Heading
будет использоватьLevelContext
). - Предоставить этот контекст от компонента, который определяет данные. (
Section
будет предоставлятьLevelContext
).
Контекст позволяет родителю - даже очень далекому - предоставлять некоторые данные всему дереву внутри него.
Использование контекста в близких дочерних компонентах
Использование контекста в далеких детях
Шаг 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 |
|
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 |
|
Единственным аргументом для createContext
является значение по умолчанию. Здесь 1
означает самый большой уровень заголовка, но вы можете передать любое значение (даже объект). Значение значения по умолчанию вы увидите в следующем шаге.
Шаг 2: Использование контекста¶
Импортируйте хук 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 |
|
Заметьте, что этот пример не совсем работает! Все заголовки имеют одинаковый размер, потому что хотя вы используете контекст, вы еще не предоставили его. React не знает, где его взять!
Если вы не предоставите контекст, React будет использовать значение по умолчанию, которое вы указали в предыдущем шаге. В этом примере вы указали 1
в качестве аргумента для createContext
, поэтому useContext(LevelContext)
возвращает 1
, устанавливая все эти заголовки в <h1>
. Давайте решим эту проблему, предоставив каждой Section
свой собственный контекст.
Шаг 3: Предоставление контекста¶
Компонент 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 |
|
Это тот же результат, что и в оригинальном коде, но вам не нужно передавать свойство level
каждому компоненту Heading
! Вместо этого, он "вычисляет" уровень заголовка, спрашивая ближайший Section
выше:
- Вы передаете пропс
level
компоненту<Section>
. Section
оборачивает свои дочерние элементы в<LevelContext.Provider value={level}>
.Heading
запрашивает ближайшее значениеLevelContext
выше с помощьюuseContext(LevelContext)
.
Использование и предоставление контекста из одного и того же компонента¶
В настоящее время вы все еще должны указывать параметр level
каждой секции вручную:
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 |
|
Теперь и Heading
, и Section
читают LevelContext
, чтобы определить, насколько "глубоко" они находятся. А Section
оборачивает свои дочерние компоненты в LevelContext
, чтобы указать, что все, что находится внутри него, находится на более "глубоком" уровне.
Пояснение примера
В этом примере используются уровни заголовков, потому что они наглядно показывают, как вложенные компоненты могут переопределять контекст. Но контекст полезен и во многих других случаях. Вы можете передать любую информацию, необходимую всему поддереву: текущую цветовую тему, пользователя, вошедшего в систему, и так далее.
Контекст проходит через промежуточные компоненты¶
Между компонентом, который предоставляет контекст, и компонентом, который его использует, можно вставить сколько угодно компонентов. Сюда входят как встроенные компоненты типа <div>
, так и компоненты, которые вы можете создать самостоятельно.
В этом примере один и тот же компонент Post
(с пунктирной границей) отображается на двух разных уровнях вложенности. Обратите внимание, что <Heading>
внутри него получает свой уровень автоматически от ближайшего <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 |
|
Вы не сделали ничего особенного, чтобы это сработало. Секция Section
определяет контекст для дерева внутри нее, поэтому вы можете вставить <Heading>
куда угодно, и он будет иметь правильный размер. Попробуйте это в песочнице выше!
Контекст позволяет вам писать компоненты, которые "адаптируются к окружению" и отображаются по-разному в зависимости от того, где (или, другими словами, в каком контексте) они отображаются.
Работа контекста может напомнить вам наследование CSS свойств. В CSS вы можете указать color: blue
для div
, и любой DOM-узел внутри него, независимо от глубины, унаследует этот цвет, если только какой-либо другой DOM-узел в середине не переопределит его с color: green
. Аналогично, в React единственный способ переопределить какой-то контекст, идущий сверху, - это обернуть дочерние элементы в провайдер контекста с другим значением.
В CSS различные свойства, такие как color
и background-color
, не отменяют друг друга. Вы можете установить color
всех div
в красный цвет, не влияя на background-color
. Аналогично, различные контексты React не отменяют друг друга. Каждый контекст, который вы создаете с помощью createContext()
, полностью отделен от других, и связывает вместе компоненты, использующие и предоставляющие этот конкретный контекст. Один компонент может использовать или предоставлять множество различных контекстов без проблем.
Прежде чем использовать контекст¶
Контекст очень заманчиво использовать! Однако, это также означает, что его слишком легко использовать чрезмерно. Если вам нужно передать некоторые пропсы на несколько уровней вглубь, это не значит, что вы должны поместить эту информацию в контекст.
Вот несколько альтернатив, которые вы должны рассмотреть, прежде чем использовать контекст:
- Начните с передачи пропсов Если ваши компоненты не тривиальны, нет ничего необычного в том, чтобы передать дюжину пропсов вниз через дюжину компонентов. Это может показаться трудоемкой задачей, но так становится ясно, какие компоненты используют те или иные данные! Человек, обслуживающий ваш код, будет рад, что вы сделали поток данных явным с помощью пропсов.
- Извлекайте компоненты и передавайте JSX как
children
к ним. Если вы передаете некоторые данные через множество уровней промежуточных компонентов, которые не используют эти данные (и передают их только дальше вниз), это часто означает, что вы забыли извлечь некоторые компоненты по пути. Например, вы передаете пропсы данных, такие какposts
, визуальным компонентам, которые не используют их напрямую, например<Layout posts={posts} />
. Вместо этого, заставьтеLayout
приниматьchildren
в качестве пропса и выводить<Layout><Posts posts={posts} /></Layout>
. Это уменьшает количество уровней между компонентом, задающим данные, и компонентом, которому они нужны.
Если ни один из этих подходов вам не подходит, подумайте о контексте.
Примеры использования контекста¶
- Тематика: Если ваше приложение позволяет пользователю изменять его внешний вид (например, темный режим), вы можете поместить провайдер контекста в верхней части вашего приложения,
- Текущая учетная запись: Многим компонентам может понадобиться знать текущего пользователя, вошедшего в систему. Если поместить его в контекст, его будет удобно прочитать в любом месте дерева. Некоторые приложения также позволяют работать с несколькими учетными записями одновременно (например, оставлять комментарии под другим пользователем). В этих случаях может быть удобно обернуть часть пользовательского интерфейса во вложенный провайдер с другим значением текущего счета.
- Маршрутизация: Большинство решений для маршрутизации используют внутренний контекст для хранения текущего маршрута. Так каждая ссылка "знает", активна она или нет. Если вы создаете свой собственный маршрутизатор, вы, возможно, захотите сделать это тоже.
- Управление состоянием: По мере роста вашего приложения, вы можете оказаться с большим количеством состояния ближе к вершине приложения. Многие удаленные компоненты внизу могут захотеть его изменить. Обычно используется reducer вместе с контекстом для управления сложным состоянием и передачи его вниз к удаленным компонентам без лишних проблем.
Контекст не ограничивается статическими значениями. Если при следующем рендере вы передадите другое значение, React обновит все компоненты, читающие его ниже! Вот почему контекст часто используется в сочетании с состоянием.
В общем, если какая-то информация нужна удаленным компонентам в разных частях дерева, это верный признак того, что контекст вам поможет.
Итого
- Контекст позволяет компоненту предоставлять некоторую информацию всему дереву под ним.
- Чтобы передать контекст:
- Создайте и экспортируйте его с помощью
export const MyContext = createContext(defaultValue)
. - Передайте его в хук
useContext(MyContext)
, чтобы прочитать его в любом дочернем компоненте, независимо от его глубины. - Заверните дочерние компоненты в
<MyContext.Provider value={...}>
, чтобы предоставить его от родителя.
- Создайте и экспортируйте его с помощью
- Контекст проходит через любые компоненты в середине.
- Контекст позволяет вам писать компоненты, которые "адаптируются к своему окружению".
- Прежде чем использовать контекст, попробуйте передать props или передать JSX в качестве
children
.
Задача¶
1. Замените сверление пропса контекстом¶
В этом примере переключение флажка изменяет параметр 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 |
|
Показать решение
Удалите свойство 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 |
|
Обратите внимание, что компонентам в середине больше не нужно передавать imageSize
.
Источник — https://react.dev/learn/passing-data-deeply-with-context