Как работать с контекстом в 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>? Для этого дочернему компоненту потребуется какой-то способ «попросить» данные откуда-то сверху на дереве. Вы не можете сделать это только с пропсами. Именно здесь в игру вступает контекст. Вы можете сделать это в три шага:
  1. Создайте контекст. (Вы можете назвать его LevelContext, так как он предназначен для уровня заголовка)
  2. Используйте этот контекст из компонента, которому требуются данные. (Heading будет использовать LevelContext)
  3. Укажите этот контекст из компонента, задающего данные. (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:
<Section level={4}>
  <Heading>Sub-sub-heading</Heading>
  <Heading>Sub-sub-heading</Heading>
  <Heading>Sub-sub-heading</Heading>
</Section>
Обратите внимание, что этот пример еще не совсем работает. Все заголовки имеют одинаковый размер, потому что хоть мы и вызвали контекст, но еще не предоставили его. React не знает, где его взять. Если контекст не указан, React будет использовать значение по умолчанию, указанное на предыдущем шаге. В этом примере в качестве аргумента createContext мы передали 1, поэтому useContext(LevelContext) возвращает 1, установив для всех этих заголовков <h1>. Давайте исправим эту проблему, предоставив каждому Section свой собственный контекст.

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

Компонент Section в настоящее время рендерит дочерние компоненты. Оберните их поставщиком контекста (context provider), чтобы предоставить им LevelContext:
import { LevelContext } from './LevelContext.js';

export default function Section({ level, children }) {
  return (
    <section className="section">
      <LevelContext.Provider value={level}>
        {children}
      </LevelContext.Provider>
    </section>
  );
}
Это говорит React: «Если какой-либо компонент внутри этого <Section> просит LevelContext, дайте им этот level». Компонент будет использовать значение ближайшего <LevelContext.Provider> в UI дереве над ним. Этот код будет работать так же как и в самой первой реализации. Но вам не нужно было передавать проп level каждому компоненту Heading. Вместо этого он «выясняет» свой уровень заголовка, спрашивая ближайший Section выше:
  1. Вы передаете проп level в <Section>.
  2. Section оборачивает свои дочерние элементы в <LevelContext.Provider value={level}>.
  3. Heading запрашивает ближайшее значение LevelContext выше с useContext(LevelContext).

Типичные варианты использования контекста

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

Итоги

  • Контекст позволяет компоненту предоставлять некоторую информацию всему дереву под ним.
  • Чтобы передать контекст:
    1. Создайте и экспортируйте его с export const MyContext = createContext(defaultValue).
    2. Передайте его в хук useContext(MyContext) чтобы прочитать его в любом дочернем компоненте, независимо от глубины.
    3. Оберните дочерние комоненты в <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;