Как работать с контекстом в 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.

Как переключать css класс в React

год назад·2 мин. на чтение

В этой статье рассмотрим, как динамически переключать css классы в приложении React.

Переключение ccs классов

Чтобы переключить css класс, нам нужно использовать логическое значение, например, в тернарном выражении.
  • Если логическое значение true, имя класса добавляется к элементу react.
  • Если логическое значение false имя класса удаляется из элемента react.
Ниже приведен пример, в котором используются React хуки, которые добавляют css класс app в элемент div, когда мы нажимаем кнопку Toggle class. Если мы нажмем эту кнопку еще раз, имя класса будет удалено из элемента div.
// App.js

import React, { useState } from "react";
import "./styles.css";

export default function App() {
  const [isActive, setActive] = useState(false);

  const handleToggle = () => {
    setActive(!isActive);
  };
  return (
    <div className={isActive ? "app" : null}>
      <h1>Hello react</h1>
      <button onClick={handleToggle}>Toggle class</button>
    </div>
  );
}
В компонентах, основанных на классах, вы можете сделать это следующим образом.
// App.js

import React, { Component } from "react";
import "./styles.css";

class App extends Component {
  state = { isActive: false };

  handleToggle = () => {
    this.setState({ isActive: !this.state.isActive });
  };

  render() {
    const isActive = this.state.isActive;
    return (
      <div className={isActive ? "app" : null}>
        <h1>Hello react</h1>
        <button onClick={this.handleToggle}>Toggle class</button>
      </div>
    );
  }
}

export default App;

Переключение между двумя именами классов

Вместо того, чтобы добавлять или удалять имя класса, мы также можем переключаться между двумя именами классов в приложении React. В приведенном ниже примере по умолчанию класс container добавляется в элемент div, когда мы нажимаем кнопку Toggle class класс container удаляется, а класс app добавляется к элементу.
// App.js

import React, { useState } from "react";
import "./styles.css";

export default function App() {
  const [isActive, setActive] = useState(false);

  const handleToggle = () => {
    setActive(!isActive);
  };
  return (
    <div className={isActive ? "app" : "container"}>
      <h1>Hello react</h1>
      <button onClick={handleToggle}>Toggle class</button>
    </div>
  );
}

Добавление имени класса к существующему имени класса

Вы также можете добавить новое имя класса к существующему имени класса следующим образом.
// App.js

import React, { useState } from "react";
import "./styles.css";

export default function App() {
  const [isActive, setActive] = useState(false);

  const handleToggle = () => {
    setActive(!isActive);
  };
  return (
    <div className={`app ${isActive ? "danger" : ""}`}>
      <h1>Hello react</h1>
      <button onClick={handleToggle}>Toggle class</button>
    </div>
  );
}
Первоначальный вид html разметки:
<div class="app">
   <h1>Hello react</h1>
   <button>Toggle class</button>
</div>
После нажатия кнопки Toggle class html выглядит следующим образом.
<div class="app danger">
   <h1>Hello react</h1>
   <button>Toggle class</button>
</div>