Как работать с контекстом в 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
.
Как вызвать метод дочернего компонента из родительского компонента с помощью useImperativeHandle
2 года назад·3 мин. на чтение
Быстрый старт с useImperativeHandle
В этой статье будет показано, как вызвать метод дочернего компонента с помощью ссылки. Чтобы решить эту проблему, мы будем использовать хуки
Хук
Теперь давайте сосредоточимся на нашей задаче. Мы хотим вызвать метод (
На этом этапе мы можем создать ссылку в родительском компоненте с помощью хука
В родительский компонент нам нужно импортировать этот
useRef
и useImperativeHandle
.
Дочерний компонент
Начнем с простого дочернего компонента, в котором содержится кнопка. Нажатие на кнопку вызывает внутренний методdoSomething
.
// Child.jsx function Child(props, ref) { const doSomething = () => { console.log("do something"); }; return ( <div> <h1>Child Component</h1> <button onClick={doSomething}>Run</button> </div> ); } export default Child;
Родительский компонент
Далее рассмотрим родительский компонент. В нем используется дочерний компонент, описанный выше. Обратите внимание, что в родительском компоненте есть собственная кнопка сохранения.// App.jsx import Child from "./Child"; function App() { const save = () => {}; return ( <div> <Child /> <button onClick={save}>Save</button> </div> ); } export default App;
Хук useImperativeHandle
Теперь давайте сосредоточимся на нашей задаче. Мы хотим вызвать метод (doSomething
) дочернего компонента при нажатии кнопки (Save
) из родительского компонента.
Чтобы вызвать метод из дочернего компонента, нам нужно сначала выставить его наружу.
useImperativeHandle
определяет значение объекта, которое предоставляется родительскому компоненту при использовании ref
. Добавляя наш метод к этому объекту, мы делаем его доступным в родительских компонентах.
// Child.jsx import { useImperativeHandle } from "react"; function Child(props, ref) { const doSomething = () => { console.log("do something"); }; useImperativeHandle(ref, () => ({ doSomething })); return ( <div> <h1>Child Component</h1> <button onClick={doSomething}>Run</button> </div> ); } export default Child;
useImperativeHandle
следует использовать с forwardRef
.
forwardRef
позволяет родительскому компоненту передавать ссылки своим дочерним элементам. Чтобы прикрепить функции или поля к этой ссылке (к рефу), используется хук useImperativeHandle
.
// Child.jsx import { forwardRef, useImperativeHandle } from "react"; function Child(props, ref) { const doSomething = () => { console.log("do something"); }; useImperativeHandle(ref, () => ({ doSomething })); return ( <div> <h1>Child Component</h1> <button onClick={doSomething}>Run</button> </div> ); } export default forwardRef(Child); // Child обернут в forwardRef
useRef
и передать ее дочернему компоненту. Получив эту ссылку, мы можем вызвать метод doSomething
дочернего компонента.
// App.jsx import { useRef } from "react"; import Child from "./Child"; function App() { const childRef = useRef(null); const save = () => { if (childRef.current) { childRef.current.doSomething(); } }; return ( <div> <Child ref={childRef} /> <button onClick={save}>Save</button> </div> ); } export default App;
Добавим TypeScript
Далее посмотрим, какие изменения нужно сделать, чтобы вызвать тот же дочерний метод из родительского компонента при использовании TypeScript. Во-первых, нам нужно определить новый интерфейс, содержащий метод, который будет представлен.Затем новый тип (export interface RefType { doSomething: () => void; }
RefType
) используется при получении ссылки в дочернем компоненте.
Ниже приведен полный код дочернего компонента.function Child(props: PropsType, ref: Ref<RefType>)
// Child.jsx import { forwardRef, useImperativeHandle, Ref } from "react"; export interface PropsType {} export interface RefType { doSomething: () => void; } function Child(props: PropsType, ref: Ref<RefType>) { const doSomething = () => { console.log("do something"); }; useImperativeHandle(ref, () => ({ doSomething })); return ( <div> <h1>Child Component</h1> <button onClick={doSomething}>Run</button> </div> ); } export default forwardRef(Child);
RefType
, содержащий все публичные дочерние методы, и использовать его при создании ref
.
Полный код родительского компонента.// App.jsx import Child, { RefType } from "./Child"; //... const childRef = useRef<RefType>(null);
import { useRef } from "react"; import Child, { RefType } from "./Child"; function App() { const childRef = useRef<RefType>(null); const save = () => { if (childRef.current) { childRef.current.doSomething(); } }; return ( <div> <Child ref={childRef} /> <button onClick={save}>Save</button> </div> ); } export default App;