Как работать с контекстом в React?
2 года назад·5 мин. на чтение
Контекст позволяет родительскому компоненту сделать некоторую информацию доступной для любого компонента в дереве под ним — независимо от того, насколько он глубок — без явной передачи ее через пропсы.
Обычно вы передаете информацию от родительского компонента дочернему компоненту через пропс. Но передача пропса может стать многословной и неудобной, если вам придется передавать их через множество компонентов посередине или если многим компонентам вашего приложения требуется одна и та же информация.
Допустим, вы хотите, чтобы несколько заголовков в одном разделе всегда имели одинаковый размер:
Проблема с передачей пропсов в React
Передача пропсов - отличный способ явно передать данные через UI дерево компонентам, которые его используют. Но передача пропса может стать многословным и неудобным, когда вам нужно передать какой-то проп глубоко через дерево, или если многим компонентам нужен один и тот же проп. Ближайший общий предок может быть далек от компонентов, которые нуждаются в данных, и поднятие состояния вверх может привести к ситуации, иногда называемой “prop drilling”. Было бы здорово, просто «телепортировать» данные к компонентам дерева, которые в них нуждаются.Контекст: альтернатива передаче пропсов
Контекст позволяет родительскому компоненту предоставлять данные всему дереву под ним. Есть много применений для контекста. Вот один из примеров. Рассмотрим компонентHeading
, который принимает level
для своего размера:
// App.js 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.js export default function Section({ children }) { return ( <section className="section"> {children} </section> ); }
// Heading.js export default function Heading({ level, children }) { switch (level) { case 1: return <h1>{children}</h1>; case 2: return <h2>{children}</h2>; case 3: return <h3>{children}</h3>; case 4: return <h4>{children}</h4>; case 5: return <h5>{children}</h5>; case 6: return <h6>{children}</h6>; default: throw Error('Unknown level: ' + level); } }
В настоящее время вы передаете проп// App.js 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: Создайте контекст
Во-первых, вам нужно создать контекст. Вам нужно будет экспортировать его из файла, чтобы ваши компоненты могли использовать его:Единственным аргументом// LevelContext.js import { createContext } from 'react'; export const LevelContext = createContext(1);
createContext
является значение по умолчанию. Здесь 1
относится к самому большому уровню заголовка, но вы можете передать любое значение (даже объект). Вы увидите значение значения по умолчанию на следующем шаге.
Шаг 2: Используйте контекст
Импортируйте хукuseContext
из React и импортируйте контекст, созданный на первом шаге:
Как вы помните компонент// Heading.js 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
, вам больше не нужно передавать проп level
в Heading
в вашем JSX.
Обновите JSX так, чтобы его получал Section
:
Обратите внимание, что этот пример еще не совсем работает. Все заголовки имеют одинаковый размер, потому что хоть мы и вызвали контекст, но еще не предоставили его. React не знает, где его взять. Если контекст не указан, React будет использовать значение по умолчанию, указанное на предыдущем шаге. В этом примере в качестве аргумента<Section level={4}> <Heading>Sub-sub-heading</Heading> <Heading>Sub-sub-heading</Heading> <Heading>Sub-sub-heading</Heading> </Section>
createContext
мы передали 1
, поэтому useContext(LevelContext)
возвращает 1
, установив для всех этих заголовков <h1>
. Давайте исправим эту проблему, предоставив каждому Section
свой собственный контекст.
Шаг 3: Предоставьте контекст
КомпонентSection
в настоящее время рендерит дочерние компоненты. Оберните их поставщиком контекста (context provider), чтобы предоставить им LevelContext
:
Это говорит React: «Если какой-либо компонент внутри этогоimport { LevelContext } from './LevelContext.js'; export default function Section({ level, children }) { return ( <section className="section"> <LevelContext.Provider value={level}> {children} </LevelContext.Provider> </section> ); }
<Section>
просит LevelContext
, дайте им этот level
». Компонент будет использовать значение ближайшего <LevelContext.Provider>
в UI дереве над ним.
Этот код будет работать так же как и в самой первой реализации. Но вам не нужно было передавать проп level
каждому компоненту Heading
. Вместо этого он «выясняет» свой уровень заголовка, спрашивая ближайший Section
выше:
- Вы передаете проп
level
в<Section>
. Section
оборачивает свои дочерние элементы в<LevelContext.Provider value={level}>
.Heading
запрашивает ближайшее значениеLevelContext
выше сuseContext(LevelContext)
.
Типичные варианты использования контекста
- Темы: Если ваше приложение позволяет пользователю изменять его внешний вид (например, в темном режиме), вы можете поместить поставщика контекста в верхнюю часть приложения и использовать этот контекст в компонентах, которые должны настроить свой визуальный вид.
- Текущий пользователь приложения: Многим компонентам может потребоваться знать текущего вошедшего в систему пользователя. Помещение его в контекст позволяет удобно читать его в любом месте дерева. Некоторые приложения также позволяют управлять несколькими учетными записями одновременно (например, оставлять комментарий как другой пользователь). В этих случаях может быть удобно обернуть часть пользовательского интерфейса во вложенного поставщика с другим значением текущего счета.
- Маршрутизация: Большинство решений маршрутизации используют внутренний контекст для хранения текущего маршрута. Таким образом, каждая ссылка «знает», активна она или нет. Если вы создаете свой собственный маршрутизатор, вы тоже можете это сделать.
- Управление состоянием: По мере роста вашего приложения вы можете получить большое состояние ближе к верхней части вашего приложения. Многие отдаленные компоненты ниже могут захотеть изменить его. Обычно используется редьюсер вместе с контекстом для управления сложным состоянием и передачи его удаленным компонентам без особых хлопот.
Итоги
- Контекст позволяет компоненту предоставлять некоторую информацию всему дереву под ним.
-
Чтобы передать контекст:
- Создайте и экспортируйте его с
export const MyContext = createContext(defaultValue)
. - Передайте его в хук
useContext(MyContext)
чтобы прочитать его в любом дочернем компоненте, независимо от глубины. - Оберните дочерние комоненты в
<MyContext.Provider value={...}>
для представления контекста из родительского компонента.
- Создайте и экспортируйте его с
- Контекст проходит через любые компоненты посередине.
- Контекст позволяет писать компоненты, которые «адаптируются к окружающей среде».
-
Прежде чем использовать контекст, попробуйте передать проп или передать JSX как
children
.
Простая шина событий для общения между компонентами во Vue 2
2 года назад·2 мин. на чтение
Иногда нужно реализовать быстрое и простое решение для передачи данных между компонентами Vue.js.
Конечно, есть Vuex для централизованного управления состоянием. Таким образом, он может обеспечить приложению единый источник истины.
Но для приложения с простой архитектурой достаточно общаться между компонентами с помощью событий. Для этого мы можем создать быстрое решение и реализовать шину событий (event bus). Шину событий позволяет нам отправлять в одном компоненте и слушать это событие в другом.
В данном примере будет показано, как это сделать в приложении Vue.js. Благодаря простоте фреймворка Vue он позволяет нам создать механизм обмена событиями с помощью нескольких строк кода.
Здесь у нас есть компонент ComponentA, который импортирует
Здесь мы создали JavaScript ES6 модуль, который импортирует Vue, создали и экспортировали новый экземпляр Vue. Вот и все. Мы реализовали EventBus и теперь можем начать его использовать.// eventBus.js import Vue from "vue"; export const eventBus = new Vue();
eventBus
. Когда вызывается метод emitMethod()
компонента, он испускает новое событие с именем EVENT_NAME
и передает вместе с ним данные события (payload).
В другом компоненте мы можем зарегистрировать слушателя, который слушает событие// ComponentA.js <script> import { eventBus } from "./eventBus"; export default { name: "ComponentA", methods: { emitMethod() { eventBus.$emit("EVENT_NAME", { data: "someData" }); }, }, }; </script> <template> <p>ComponentA</p> </template>
EVENT_NAME
, передаваемое по шине eventBus
. Как только событие появится, мы можем выполнить JavaScript с полученным в качестве аргумента payload.
Вот и все. Вот так просто мы создали решение для передачи событий между различными частями нашего приложения.// ComponentB.js <script> import { eventBus } from "./eventBus"; export default { name: "ComponentB", mounted() { eventBus.$on("EVENT_NAME", this.handleEvent); }, beforeUnmount() { eventBus.$off("EVENT_NAME", this.handleEvent); }, methods: { handleEvent(payload) { console.log(payload); // => { data: "someData" } }, }, }; </script> <template> <p>ComponentB</p> </template>
В этом примере мы создали и реализовали механизм передачи данных между слабосвязанными компонентами. Не тратя много времени и усилий на изучение принципов Vuex, Redux или других фреймворков с неизменяемым состоянием. Это удобный способ коммуникации для более простой архитектуры. Которая впоследствии может быть усовершенствована путем внедрения какого-либо централизованного фреймворка управления состояниями по мере роста приложения.// App.js <script> import ComponentA from "./ComponentA"; import ComponentB from "./ComponentB"; export default { name: "App", components: { ComponentA, ComponentB, }, }; </script> <template> <div> <ComponentB /> <ComponentA /> </div> </template>