react的context

react的context

在一个典型的 React 应用中,数据是通过 props 属性自上而下(由父及子)进行传递的,但这种做法对于某些类型的属性而言是极其繁琐的(例如:地区偏好,UI 主题),这些属性是应用程序中许多组件都需要的。Context 提供了一种在组件之间共享此类值的方式,而不必显式地通过组件树的逐层传递 props。

何时使用Context

Context 设计目的是为了共享那些对于一个组件树而言是“全局”的数据,例如当前认证的用户、主题或首选语言。

比如,每一个按钮都需要知道theme的值,层层传递是很麻烦的事。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class App extends React.Component {
render() {
return <Toolbar theme="dark" />;
}
}

function Toolbar(props) {
return (
<ThemedButton theme={props.theme} />
);
}

class ThemedButton extends React.Component {
render() {
return <Button theme={this.props.theme} />;
}
}

使用 context, 我们可以避免通过中间元素传递 props:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28

// 为当前的 theme 创建一个 context(“light”为默认值)。
const ThemeContext = React.createContext('light');
class App extends React.Component {
render() {
// 使用一个 Provider 来将当前的 theme 传递给以下的组件树。
return (
<ThemeContext.Provider value="dark">
<Toolbar />
</ThemeContext.Provider>
);
}
}

function Toolbar() {
return (
<ThemedButton />
);
}

class ThemedButton extends React.Component {
// 指定 contextType 读取当前的 theme context。
// React 会往上找到最近的 theme Provider,然后使用它的值。
static contextType = ThemeContext;
render() {
return <Button theme={this.context} />;
}
}

使用 Context 之前的考虑

Context 主要应用场景在于很多不同层级的组件需要访问同样一些的数据。请谨慎使用,因为这会使得组件的复用性变差。

如果你只是想避免层层传递一些属性,组件组合(component composition)有时候是一个比 context 更好的解决方案。

比如,考虑这样一个 Page 组件,它层层向下传递 user 和 avatarSize 属性,从而深度嵌套的 Link 和 Avatar 组件可以读取到这些属性:

1
2
3
4
5
6
7
8
9
<Page user={user} avatarSize={avatarSize} />
// ... 渲染出 ...
<PageLayout user={user} avatarSize={avatarSize} />
// ... 渲染出 ...
<NavigationBar user={user} avatarSize={avatarSize} />
// ... 渲染出 ...
<Link href={user.permalink}>
<Avatar user={user} size={avatarSize} />
</Link>

如果在最后只有 Avatar 组件真的需要 user 和 avatarSize,那么层层传递这两个 props 就显得非常冗余。而且一旦 Avatar 组件需要更多从来自顶层组件的 props,你还得在中间层级一个一个加上去,这将会变得非常麻烦。

一种无需 context 的解决方案是将 Avatar 组件自身传递下去,因而中间组件无需知道 user 或者 avatarSize 等 props:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
function Page(props) {
const user = props.user;
const userLink = (
<Link href={user.permalink}>
<Avatar user={user} size={props.avatarSize} />
</Link>
);
return <PageLayout userLink={userLink} />;
}

// 现在,我们有这样的组件:
<Page user={user} avatarSize={avatarSize} />
// ... 渲染出 ...
<PageLayout userLink={...} />
// ... 渲染出 ...
<NavigationBar userLink={...} />
// ... 渲染出 ...
{props.userLink}

这种对组件的控制反转减少了在你的应用中要传递的 props 数量,这在很多场景下会使得你的代码更加干净,使你对根组件有更多的把控。但是,这并不适用于每一个场景:这种将逻辑提升到组件树的更高层次来处理,会使得这些高层组件变得更复杂,并且会强行将低层组件适应这样的形式,这可能不会是你想要的。

这种模式足够覆盖很多场景了,在这些场景下你需要将子组件和直接关联的父组件解耦。如果子组件需要在渲染前和父组件进行一些交流,你可以进一步使用 render props。

使用 context 的通用的场景包括管理当前的 locale,theme,或者一些缓存数据,这比替代方案要简单的多。

API

React.createContext

1
const MyContext = React.createContext(defaultValue);

创建一个 Context 对象。当 React 渲染一个订阅了这个 Context 对象的组件,这个组件会从组件树中离自身最近的那个匹配的 Provider 中读取到当前的 context 值。

只有当组件所处的树中没有匹配到 Provider 时,其 defaultValue 参数才会生效。

Context.Provider

1
<MyContext.Provider value={/* 某个值 */}>

每个 Context 对象都会返回一个 Provider React 组件,它允许消费组件订阅 context 的变化。

Provider 接收一个 value 属性,传递给消费组件。一个 Provider 可以和多个消费组件有对应关系。多个 Provider 也可以嵌套使用,里层的会覆盖外层的数据。

当 Provider 的 value 值发生变化时,它内部的所有消费组件都会重新渲染。Provider 及其内部 consumer 组件都不受制于 shouldComponentUpdate 函数,因此当 consumer 组件在其祖先组件退出更新的情况下也能更新。

通过新旧值检测来确定变化,使用了与 Object.is 相同的算法。

Class.contextType

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class MyClass extends React.Component {
componentDidMount() {
let value = this.context;
/* ... */
}
componentDidUpdate() {
let value = this.context;
/* ... */
}
componentWillUnmount() {
let value = this.context;
/* ... */
}
render() {
let value = this.context;
}
}

MyClass.contextType = MyContext;

挂载在 class 上的 contextType 属性会被重赋值为一个由 React.createContext() 创建的 Context 对象。这能让你使用 this.context 来消费最近 Context 上的那个值。你可以在任何生命周期中访问到它,包括 render 函数中。

如果你正在使用实验性的 public class fields 语法,你可以使用 static 这个类属性来初始化你的 contextType。

1
2
3
4
5
6
7
class MyClass extends React.Component {
static contextType = MyContext;
render() {
let value = this.context;
/* 基于这个值进行渲染工作 */
}
}

Context.Consumer

1
2
3
<MyContext.Consumer>
{value => /* 基于 context 值进行渲染*/}
</MyContext.Consumer>

这里,React 组件也可以订阅到 context 变更。这能让你在函数式组件中完成订阅 context。

这需要函数作为子元素(function as a child)这种做法。这个函数接收当前的 context 值,返回一个 React 节点。传递给函数的 value 值等同于往上组件树离这个 context 最近的 Provider 提供的 value 值。如果没有对应的 Provider,value 参数等同于传递给 createContext() 的 defaultValue。

Context.displayName

context 对象接受一个名为 displayName 的 property,类型为字符串。React DevTools 使用该字符串来确定 context 要显示的内容。

示例,下述组件在 DevTools 中将显示为 MyDisplayName:

1
2
3
4
5
const MyContext = React.createContext(/* some value */);
MyContext.displayName = 'MyDisplayName';

<MyContext.Provider> // "MyDisplayName.Provider" 在 DevTools 中
<MyContext.Consumer> // "MyDisplayName.Consumer" 在 DevTools 中

动态 Context

theme-context.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import React from "react";

export const themes = {
light: {
color: "yellow"
},
dark: {
color: "blue"
}
};

export const ThemeContext = React.createContext(
themes.dark // 默认值
);

themed-button.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import React from "react";
import { ThemeContext } from "./theme-context";

class ThemedButton extends React.Component {
render() {
const props = this.props;
const theme = this.context;
return (
<button {...props} style={{ backgroundColor: theme.color }}>
btn
</button>
);
}
}

ThemedButton.contextType = ThemeContext;

export default ThemedButton;

app.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
import React from "react";

import { ThemeContext, themes } from "./theme-context";
import ThemedButton from "./ThemedButton.jsx";

// 一个使用 ThemedButton 的中间组件
function Toolbar(props) {
return <ThemedButton onClick={props.changeTheme} />;
}

class App extends React.Component {
constructor(props) {
super(props);
this.state = {
theme: themes.light
};

this.toggleTheme = () => {
this.setState(state => ({
theme: state.theme === themes.dark ? themes.light : themes.dark
}));
};
}

render() {
return (
<ThemeContext.Provider value={this.state.theme}>
<Toolbar changeTheme={this.toggleTheme} />
</ThemeContext.Provider>
);
}
}

export default App;

对于上面的 theme 例子,使用动态值(dynamic values)后更复杂的用法:

在嵌套组件中更新 Context

从一个在组件树中嵌套很深的组件中更新 context 是很有必要的。在这种场景下,你可以通过 context 传递一个函数,使得 consumers 组件更新 context。

theme-context.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import React from "react";

export const themes = {
light: {
color: "yellow"
},
dark: {
color: "blue"
}
};

export const ThemeContext = React.createContext({
theme: themes.dark,
toggleTheme: () => {}
});

theme-toggler-button.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import React from "react";
import { ThemeContext } from "./theme-context";

function ThemeTogglerButton() {
return (
<ThemeContext.Consumer>
{({ theme, toggleTheme }) => (
<button onClick={toggleTheme} style={{ backgroundColor: theme.color }}>
Toggle Theme
</button>
)}
</ThemeContext.Consumer>
);
}

export default ThemeTogglerButton;

app.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
import React from "react";

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>
);
}

export default App;

消费多个 Context

为了确保 context 快速进行重渲染,React 需要使每一个 consumers 组件的 context 在组件树中成为一个单独的节点。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
import React from "react";

const ThemeContext = React.createContext("light");

const UserContext = React.createContext("");

class App extends React.Component {
render() {
return (
<ThemeContext.Provider value={"light"}>
<UserContext.Provider value={"user"}>
<Layout />
</UserContext.Provider>
</ThemeContext.Provider>
);
}
}

function Layout() {
return <Content />;
}

function Content() {
return (
<ThemeContext.Consumer>
{theme => (
<UserContext.Consumer>
{user => (
<div>
{user} {theme}
</div>
)}
</UserContext.Consumer>
)}
</ThemeContext.Consumer>
);
}

export default App;

如果两个或者更多的 context 值经常被一起使用,那你可能要考虑一下另外创建你自己的渲染组件,以提供这些值。

注意事项

因为 context 会使用参考标识(reference identity)来决定何时进行渲染,这里可能会有一些陷阱,当 provider 的父组件进行重渲染时,可能会在 consumers 组件中触发意外的渲染。

举个例子,当每一次 Provider 重渲染时,以下的代码会重渲染所有下面的 consumers 组件,因为 value 属性总是被赋值为新的对象:

1
2
3
4
5
6
7
8
9
class App extends React.Component {
render() {
return (
<MyContext.Provider value={{something: 'something'}}>
<Toolbar />
</MyContext.Provider>
);
}
}

为了防止这种情况,将 value 状态提升到父节点的 state 里:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class App extends React.Component {
constructor(props) {
super(props);
this.state = {
value: {something: 'something'},
};
}

render() {
return (
<Provider value={this.state.value}>
<Toolbar />
</Provider>
);
}
}
0%