React Context для многих стал привычным способом управления состоянием, заменив собой Redux. В этой статье вы узнаете о React Context и научитесь его использовать.
Рассмотрим работу Context на примере такого дерева. Нижние блоки можно представить как отдельные компоненты:
Допустим, вам нужно добавить свойство какому-то из нижних блоков — это несложно. Но что делать, если это свойство нужно передать соседнему (т. е. отдельному) блоку? Пока единственное решение — передать это свойство блоку-родителю, откуда его можно передать необходимому дочернему блоку.
Если вдруг вам понадобится передать свойство соседнему родителю, вы, опять же, просто переносите его на уровень выше, а потом «спускаете» обратно к нужному блоку.
Решение довольно простое, а главное — рабочее. Но что делать, если нужно передать свойство дальнему блоку?
Для этого нужно «поднять» свойство по всему дереву вверх до самого первого блока, а потом «спустить» обратно к нужному дочернему блоку. Проблема в том, что это свойство будет проходить через кучу промежуточных компонентов. Этот утомительный и трудоёмкий процесс известен как пробрасывание (англ. prop drilling).
Именно на этом этапе задействуется Context API. Он даёт возможность передавать свойства отдельным блокам дерева без сложных манипуляций с родительскими и дочерними блоками.
В качестве примера использования React Context возьмём вот такой забавный переключатель дня и ночи:
Полный код можно посмотреть здесь.
Создание 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
Теперь эта кнопка переключает день и ночь.
Рекомендации к работе с Context
Хоть в этом коде всё работает отлично, всё же есть некоторые аспекты с работой Context:
- Не используйте Context, если он заменяет пробрасывание всего на один-два уровня. Этот инструмент — отличный способ, если нужно распространить состояние на множество компонентов, находящихся в «дереве» далеко друг от друга. Но если вам нужно просто опуститься или подняться на пару уровней, то пробрасывание будет легче и быстрее.
- Постарайтесь не использовать Context для сохранения локального состояния. Например, если вам нужно сохранить введённые в форму данные, то лучше использовать локальное свойство.
- Всегда оборачивайте родителя в Provider’а на как можно более низком уровне — не стоит использовать самую верхушку «дерева».
- Наконец, если вы решили пересылать свойства таким способом, важно помнить про наблюдение за производительностью и рефакторингом. Но это скорее всего не понадобится, если просадки в производительности не будут сильно заметны.
Подумываете освоить или освежить знания по React? Тогда держите дорожную карту по React-разработке.
Перевод статьи «React Context in 5 Minutes»