Обложка статьи «React Context за 5 минут: что это и как использовать»

React Context за 5 минут: что это и как использовать

Перевод статьи «React Context in 5 Minutes»

React Context для многих стал привычным способом управления состоянием, заменив собой Redux. В этой статье вы узнаете о React Context и научитесь его использовать.

Рассмотрим работу Context на примере такого дерева. Нижние блоки можно представить как отдельные компоненты:

Допустим, вам нужно добавить свойство какому-то из нижних блоков — это несложно. Но что делать, если это свойство нужно передать соседнему (т. е. отдельному) блоку? Пока единственное решение — передать это свойство блоку-родителю, откуда его можно передать необходимому дочернему блоку.

Дерево из компонентов

Если вдруг вам понадобится передать свойство соседнему родителю, вы, опять же, просто переносите его на уровень выше, а потом «спускаете» обратно к нужному блоку.

Дерево из компонентов

Решение довольно простое, а главное — рабочее. Но что делать, если нужно передать свойство дальнему блоку?

Дерево из компонентов

Для этого нужно «поднять» свойство по всему дереву вверх до самого первого блока, а потом «спустить» обратно к нужному дочернему блоку. Проблема в том, что это свойство будет проходить через кучу промежуточных компонентов. Этот утомительный и трудоёмкий процесс известен как пробрасывание (англ. prop drilling).

Дерево из компонентов

Именно на этом этапе задействуется Context API. Он даёт возможность передавать свойства отдельным блокам дерева без сложных манипуляций с родительскими и дочерними блоками.

Дерево из компонентов

В качестве примера использования React Context возьмём вот такой забавный переключатель дня и ночи:

Приложение на React

Полный код можно посмотреть здесь.

Создание Context

Вначале нужно сделать так, чтобы всё приложение имело доступ к Context. Для этого в index.js нужно обернуть всё приложение в ThemeContext.Provider. Ещё стоит передать ему свойство value. В нём будет храниться состояние: день или ночь.

import React from "react";
import ReactDOM from "react-dom";
import ThemeContext from "./themeContext";

import App from "./App";

ReactDOM.render(
  <ThemeContext.Provider value={"Day"}>
    <App />
  </ThemeContext.Provider>,
  document.getElementById("root")
);

Получение свойств от Context через contextType

Пока что в App.js возвращается компонент <Image />.

import React from "react";
import Image from "./Image";

class App extends React.Component {
  render() {
    return (
      <div className="app">
        <Image />
      </div>
    );
  }
}

export default App;

Нам нужно с помощью Context менять className в Image.js с Day на Night и обратно. Для этого нужно добавить к компоненту статическое свойство ContextType. Потом, используя интерполяцию строки, нужно передать это свойство в className в объекте <Image />.

Теперь свойство className содержит строку из value:

import React from "react";
import Button from "./Button";
import ThemeContext from "./themeContext";

class Image extends React.Component {
  render() {
    const theme = this.context;
    return (
      <div className={`${theme}-image image`}>
        <div className={`${theme}-ball ball`} />
        <Button />
      </div>
    );
  }
}

Image.contextType = ThemeContext;

export default Image;

Получение свойств из Context

К сожалению, способ выше работает только с классовыми компонентами. Но благодаря хукам с помощью функциональных компонентов теперь можно сделать всё что угодно. Так что для полноты картины нужно конвертировать имеющиеся компоненты в функциональные и использовать ThemeContext.Consumer, чтобы передать информацию между ними.

Это можно сделать, обернув элементы в экземпляр <ThemeContext.Consumer>. Внутри него нужно предоставить функцию, возвращающую элементы. В данном случае будет использоваться паттерн «render props», который позволяет передать компоненту в качестве children любую функцию, которая возвращает JSX код.

import React from "react";
import Button from "./Button";
import ThemeContext from "./themeContext";

function Image(props) {
  // Это больше не нужно
  // const theme = this.context

  return (
    <ThemeContext.Consumer>
      {theme => (
        <div className={`${theme}-image image`}>
          <div className={`${theme}-ball ball`} />
          <Button />
        </div>
      )}
    </ThemeContext.Consumer>
  );
}

// Это больше не нужно
// Image.contextType = ThemeContext;

export default Image;

Примечание <Button /> тоже нужно обернуть в <ThemeContext.Consumer> — в будущем это добавит функциональности кнопке.

import React from "react";
import ThemeContext from "./themeContext";

function Button(props) {
  return (
    <ThemeContext.Consumer>
      {context => (
        <button className="button">
          Switch
          <span role="img" aria-label="sun">
            🌞
          </span>
          <span role="img" aria-label="moon">
            🌚
          </span>
        </button>
      )}
    </ThemeContext.Consumer>
  );
}

export default Button;

Вынесение свойств из Context

На текущем этапе в приложении передаётся заранее прописанное значение, но наша цель — переключать день и ночь кнопкой. Для этого нужно переместить <Provider> в отдельный файл и обернуть его в собственный компонент ThemeContextProvider.

import React, { Component } from "react";
const { Provider, Consumer } = React.createContext();

class ThemeContextProvider extends Component {
  render() {
    return <Provider value={"Day"}>{this.props.children}</Provider>;
  }
}

export { ThemeContextProvider, Consumer as ThemeContextConsumer };

Примечание Теперь свойство value обрабатывается и в новом файле ThemeContext.js, поэтому обработку этого значения из файла index.js нужно убрать.

Изменение Context

Чтобы подвязать кнопку, сначала нужно добавить состояния state в ThemeContextProvider:

import React, { Component } from "react";
const { Provider, Consumer } = React.createContext();

// Примечание: ещё вы можете использовать хуки, чтобы определять состояние 
// и преобразовывать его в функциональный компонент
class ThemeContextProvider extends Component {
  state = {
    theme: "Day"
  };
  render() {
    return <Provider value={"Day"}>{this.props.children}</Provider>;
  }
}

export { ThemeContextProvider, Consumer as ThemeContextConsumer };

Потом нужно добавить метод переключения между днём и ночью:

toggleTheme = () => {
  this.setState(prevState => {
    return {
      theme: prevState.theme === "Day" ? "Night" : "Day"
    };
  });
};

После этого нужно изменить значение value на this.state.theme, чтобы свойство устанавливалось из состояния:

 render() {
    return <Provider value={this.state.theme}>{this.props.children}</Provider>;
  }

Теперь нужно изменить value на объект, содержащий {theme: this.state.theme, toggleTheme: this.toggleTheme}, а также заменить использование value на получение поля theme из объекта. То есть нужно каждое theme заменить на context, а каждую ссылку на theme — на context.theme.

И под конец на кнопку нужно повесить слушатель события onClick. При нажатии кнопки должен вызываться context.toggleTheme — в таком случае будут обновляться Consumer’ы, которые используют состояние от Provider’ов. Код кнопки будет выглядеть примерно так:

import React from "react";
import { ThemeContextConsumer } from "./themeContext";

function Button(props) {
  return (
    <ThemeContextConsumer>
      {context => (
        <button onClick={context.toggleTheme} className="button">
          Switch
          <span role="img" aria-label="sun">
            🌞
          </span>
          <span role="img" aria-label="moon">
            🌚
          </span>
        </button>
      )}
    </ThemeContextConsumer>
  );
}

export default Button

Теперь эта кнопка переключает день и ночь.

Приложение на React

Рекомендации к работе с Context

Хоть в этом коде всё работает отлично, всё же есть некоторые аспекты с работой Context:

  • Не используйте Context, если он заменяет пробрасывание всего на один-два уровня. Этот инструмент — отличный способ, если нужно распространить состояние на множество компонентов, находящихся в «дереве» далеко друг от друга. Но если вам нужно просто опуститься или подняться на пару уровней, то пробрасывание будет легче и быстрее.
  • Постарайтесь не использовать Context для сохранения локального состояния. Например, если вам нужно сохранить введённые в форму данные, то лучше использовать локальное свойство.
  • Всегда оборачивайте родителя в Provider’а на как можно более низком уровне — не стоит использовать самую верхушку «дерева».
  • Наконец, если вы решили пересылать свойства таким способом, важно помнить про наблюдение за производительностью и рефакторингом. Но это скорее всего не понадобится, если просадки в производительности не будут сильно заметны.

Не смешно? А здесь смешно: @ithumor