3.9 Контекст
Контекст обеспечивает способ передачи данных через дерево компонентов без необходимости передавать свойства вручную на каждом уровне.
В типичном приложении React данные передаются сверху вниз (от родителя к потомку) через
свойства props
. Однако это может оказаться громоздким для определенных типов свойств
(тема UI; предпочтения, связанные с локалью), которые требуются для многих
компонентов в приложении. Контекст предоставляет способ совместного использования таких
значений между компонентами без необходимости явно передавать свойство через каждый уровень дерева.
- 3.9.1 Когда следует использовать контекст?
- 3.9.2 API.
- 3.9.3 Примеры.
- 3.9.4 Предостережения.
- 3.9.5 Устаревший API.
Контекст разработан для совместного использования данных, которые можно
рассматривать «глобальными» для дерева React-компонентов, например таких как
текущий аутентифицированный пользователь, тема или предпочтительный язык. В
приведенном ниже коде мы вручную передаем свойство «theme», чтобы стилизовать компонент Button
:
class App extends React.Component {
render() {
return <Toolbar theme="dark" />;
}
}
function Toolbar(props) {
// Компонент Toolbar должен принимать дополнитеольное свойство "theme"
// и передавать его в компонент ThemedButton. Это может стать настоящей головной болью
// если каждая отдельная кнопка в приложении нуждается в значении свойства theme,
// потому что оно должно быть передано через все компоненты.
return (
<div>
<ThemedButton theme={props.theme} />
</div>
);
}
function ThemedButton(props) {
return <Button theme={props.theme} />;
}
Используя контекст, мы можем избежать передачи свойств через промежуточные элементы:
// Контекст позволяет нам передавать значение глубоко в дерево компонентов
// без его явной передачи через каждый компонент.
// Создайте контекст для текущей темы (значение "light" по умолчанию).
const ThemeContext = React.createContext('light');
class App extends React.Component {
render() {
// Используйте Provider, чтобы передать текущую тему вглубь дерева.
// Любой компонент может считать её, вне зависимости от того как глубоко она находится.
// В данном примере, мы передаем "dark" как текущее значение.
return (
<ThemeContext.Provider value="dark">
<Toolbar />
</ThemeContext.Provider>
);
}
}
// Промежуточному компоненту необязательно
// явно передавать тему кому-либо далее.
function Toolbar(props) {
return (
<div>
<ThemedButton />
</div>
);
}
function ThemedButton(props) {
// Используйте Consumer, чтобы считать текущий контекст темы.
// React будет искать выше ближайший поставщик (Provider) темы и использует его значение.
// В данном примере текущая тема имеет значение "dark".
return (
<ThemeContext.Consumer>
{theme => <Button {...props} theme={theme} />}
</ThemeContext.Consumer>
);
}
Внимание!
Не используйте контекст, чтобы избежать передачи свойств на несколько уровней ниже. Реальная необходимость возникает в случаях, когда одни и те же данные должны быть доступны во многих компонентах на разных уровнях.3.9.2.1 React.createContext.
const {Provider, Consumer} = React.createContext(defaultValue);
Создает пару потребитель и поставщик(провайдер): {Provider, Consumer}
. Когда
React будет отрисовывать потребителя контекста Consumer
,
он считает текущее значение контекста из ближайшего соответствующего поставщика Provider
выше в дереве иерархии.
Аргумент defaultValue
используется при отрисовке потребителя контекста, не имеющего соответствующего поставщика
выше него в дереве. Это может быть полезно для тестирования компонентов изолированно, без их обертывания.
3.9.2.2 Provider.
<Provider value={/* some value */}>
Компонент React, который позволяет потребителям подписываться на изменения контекста.
Принимает свойство value
, которое должно быть передано потребителям, которые являются
потомками данного провайдера. Один провайдер может быть связан со многими потребителями. Провайдеры
могут быть вложенными, чтобы переопределять значения глубже в дереве.
3.9.2.3 Consumer.
<Consumer>
{value => /* отрисовывает что-то, что основано на значении контекста */}
</Consumer>
Компонент React, который подписывается на изменения контекста.
Требует функцию в качестве дочернего элемента. Функция получает текущее значение
контекста и возвращает узел React. Аргумент value
, переданный функции, будет равен
свойству value
ближайшего поставщика для этого контекста выше в дереве. Если для данного
контекста нет провайдера, аргумент value
будет равен значению defaultValue
,
которое было передано в createContext()
.
Все потребители перерисовываются всякий раз при изменении значения поставщика.
Изменения определяются путем сравнения старых и новых значений с использованием
того же алгоритма, что и в Object.is
. (Это может вызвать некоторые проблемы при
передаче объектов в качестве value
: см. раздел
Предостережения
.)
3.9.3.1 Динамический контекст
Более сложный пример с динамическими значениями для темы:
theme-context.js
export const themes = {
light: {
foreground: '#ffffff',
background: '#222222',
},
dark: {
foreground: '#000000',
background: '#eeeeee',
},
};
export const ThemeContext = React.createContext(
themes.dark // значеине по умолчанию
);
themed-button.js
import {ThemeContext} from './theme-context';
function ThemedButton(props) {
return (
<ThemeContext.Consumer>
{theme => (
<button
{...props}
style={{backgroundColor: theme.background}}
/>
)}
</ThemeContext.Consumer>
);
}
export default ThemedButton;
app.js
import {ThemeContext, themes} from './theme-context';
import ThemedButton from './themed-button';
// Промежуточный компонент, который использует ThemedButton
function Toolbar(props) {
return (
<ThemedButton onClick={props.changeTheme}>
Change Theme
</ThemedButton>
);
}
class App extends React.Component {
constructor(props) {
super(props);
this.state = {
theme: themes.light,
};
this.toggleTheme = () => {
this.setState(state => ({
const { light, dark } = themes
theme: state.theme === dark ? light : dark
}));
};
}
render() {
// Кнопка ThemedButton внутри ThemeProvider
// использует тему из состояния, в то время как снаружи
// использует тему по умолчанию: dark
return (
<Page>
<ThemeContext.Provider value={this.state.theme}>
<Toolbar changeTheme={this.toggleTheme} />
</ThemeContext.Provider>
<Section>
<ThemedButton />
</Section>
</Page>
);
}
}
ReactDOM.render(<App />, document.root);
3.9.3.2 Обновление контекста из вложенного компонента
Часто необходимо обновить контекст из компонента, который глубоко вложен в дерево компонентов. В этом случае вы можете передать функцию через контекст, чтобы позволить потребителям обновлять контекст:
theme-context.js
// Убедитесь, что форма значения по умолчанию, переданная в
// createContext соответствует форме, которую ожидают потребители!
export const ThemeContext = React.createContext({
theme: themes.dark,
toggleTheme: () => {},
});
theme-toggler-button.js
import {ThemeContext} from './theme-context';
function ThemeTogglerButton() {
// Компонент ThemeTogglerButton принимает не только тему,
// но и функцию toggleTheme из контекста
return (
<ThemeContext.Consumer>
{({theme, toggleTheme}) => (
<button
onClick={toggleTheme}
style={{backgroundColor: theme.background}}>
Toggle Theme
</button>
)}
</ThemeContext.Consumer>
);
}
export default ThemeTogglerButton;
app.js
import {ThemeContext, themes} from './theme-context';
import ThemeTogglerButton from './theme-toggler-button';
class App extends React.Component {
constructor(props) {
super(props);
this.toggleTheme = () => {
this.setState(state => ({
theme:
state.theme === themes.dark
? themes.light
: themes.dark,
}));
};
// Состояние также содержит обновляющую функцию, поэтому она
// будет передана в поставщик контекста
this.state = {
theme: themes.light,
toggleTheme: this.toggleTheme
};
}
render() {
// Состояние целиком передается в поставщик
return (
<ThemeContext.Provider value={this.state}>
<Content />
</ThemeContext.Provider>
);
}
}
function Content() {
return (
<div>
<ThemeTogglerButton />
</div>
);
}
ReactDOM.render(<App />, document.root);
3.9.3.3 Потребление множества контекстов
Чтобы поддерживать перерисовку контекста быстрой, React должен сделать каждого потребителя контекста отдельным узлом в дереве.
// Контекст темы. Светлая тема по умолчанию.
const ThemeContext = React.createContext('light');
// Контекст Signed-in пользователя
const UserContext = React.createContext();
class App extends React.Component {
render() {
const {signedInUser, theme} = this.props;
// Компонент приложения, который предоставляет начальные значения контекста
return (
<ThemeContext.Provider value={theme}>
<UserContext.Provider value={signedInUser}>
<Layout />
</UserContext.Provider>
</ThemeContext.Provider>
);
}
}
function Layout() {
return (
<div>
<Sidebar />
<Content />
</div>
);
}
// Компонент может потреблять множество контекстов
function Content() {
return (
<ThemeContext.Consumer>
{theme => (
<UserContext.Consumer>
{user => (
<ProfilePage user={user} theme={theme} />
)}
</UserContext.Consumer>
)}
</ThemeContext.Consumer>
);
}
Если два и более значения контекста часто используются вместе, вы можете
рассмотреть возможность создания вашего собственного render prop
компонента,
который будет их предоставлять.
3.9.3.4 Доступ к контексту в методах жизненного цикла
Доступ к значениям из контекста в методах жизненного цикла является довольно частой ситуацией. Вместо добавления контекста в каждый метод жизненного цикла вам просто нужно передать его как свойство, а затем работать с ним так же, как и с обычным свойством.
class Button extends React.Component {
componentDidMount() {
// this.props.theme - текущее значение контекста
}
componentDidUpdate(prevProps, prevState) {
// prevProps.theme - предыдущее значение контекста
// this.props.theme - новое значение контекста
}
render() {
const {theme, children} = this.props;
return (
<button className={theme ? 'dark' : 'light'}>
{children}
</button>
);
}
}
export default props => (
<ThemeContext.Consumer>
{theme => <Button {...props} theme={theme} />}
</ThemeContext.Consumer>
);
3.9.3.5 Потребление контекста старшим компонентом (HOC-ом)
Некоторые типы контекстов могут потребляться многими компонентами (например, тема или локализация).
Может быть утомительно явно обертывать каждую зависимость с помощью
элемента <Context.Consumer>
. Старший компонент может помочь избежать этого.
Например, компонент кнопки может потреблять контекст темы следующим образом:
const ThemeContext = React.createContext('light');
function ThemedButton(props) {
return (
<ThemeContext.Consumer>
{theme => <button className={theme} {...props} />}
</ThemeContext.Consumer>
);
}
Для нескольких компонентов это выглядит хорошо, но что, если бы мы захотели использовать контекст темы во множестве мест?
Мы могли бы создать старший компонент с withTheme
:
const ThemeContext = React.createContext('light');
// Эта функция принимает компонент...
export function withTheme(Component) {
// ... возвращает другой компонент...
return function ThemedComponent(props) {
// ... и отрисовывает обернутый компонент с темой из контекста!
// Обратите внимание, что мы с таким же успехом можем передавать
// любое дополнительное свойство
return (
<ThemeContext.Consumer>
{theme => <Component {...props} theme={theme} />}
</ThemeContext.Consumer>
);
};
}
Теперь любой компонент, который зависит от контекста темы, может легко
подписаться на него с помощью созданной нами функции withTheme
:
function Button({theme, ...rest}) {
return <button className={theme} {...rest} />;
}
const ThemedButton = withTheme(Button);
3.9.3.6 Передача ссылок ref в потребители контекста
Одна из проблем с render prop
API-интерфейсом заключается в том,
что ссылки ref
не передается автоматически обернутым элементам. Чтобы
обойти это, используйте React.forwardRef
:
fancy-button.js
class FancyButton extends React.Component {
focus() {
// ...
}
// ...
}
// Используйте контекст, чтобы передать текущую "theme" в FancyButton.
// Используйте forwardRef, чтобы с тем же успехом передавать ссылки ref в FancyButton.
export default React.forwardRef((props, ref) => (
<ThemeContext.Consumer>
{theme => (
<FancyButton {...props} theme={theme} ref={ref} />
)}
</ThemeContext.Consumer>
));
app.js
import FancyButton from './fancy-button';
const ref = React.createRef();
// Наша ссылка ref будет указывать на компонент FancyButton,
// а не на ThemeContext.Consumer, который его оборачивает.
// Это означает, что мы можем вызывать FancyButton методы как ref.current.focus()
<FancyButton ref={ref} onClick={handleClick}>
Click me!
</FancyButton>;
Поскольку контекст использует ссылочную идентификацию, чтобы определить,
когда нужно проводить перерисовку, существуют некоторые "подводные камни",
которые могут вызвать непреднамеренные отрисовки в потребителях, когда перерисовывается
родитель поставщика. Например, приведенный ниже код будет повторно отрисовывать всех
потребителей каждый раз, когда перерисовывается поставщик, потому что для value
всегда создается новый объект:
class App extends React.Component {
render() {
return (
<Provider value={{something: 'что-нибудь'}}>
<Toolbar />
</Provider>
);
}
}
Чтобы обойти это, поднимите значение в состояние родителя:
class App extends React.Component {
constructor(props) {
super(props);
this.state = {
value: {something: 'что-нибудь'}
};
}
render() {
return (
<Provider value={this.state.value}>
<Toolbar />
</Provider>
);
}
}
3.9.5 Устаревший API
Внимание!
Ранее React предоставлял экспериментальный API контекста. Устаревший API будет поддерживаться во всех релизах 16.x, но приложения, использующие его, должны мигрировать на новую версию. Он будет удален в будущей major-версии React. Ознакомиться с устаревшим API контекста вы сможете здесь.