React 高级指引之 Context
1.为什么使用 Context
首先来考虑这样一个场景,有一段文本内容用于展示主题的字体颜色及字体大小。我们可以通过组件的属性来传递,代码如下:
顶层组件,定义主题
// 引入中间组件
import IntermediaryComponents from './IntermediaryComp'
// 顶层组件,定义主题
class Context extends React.Component {
constructor(props) {
super(props);
this.state = {
theme: {
color: 'light', // light,dark
size: 'small', // small, middle, large
}
}
}
render() {
const { theme } = this.state;
return <IntermediaryComponents theme={theme} />
}
}
export default Context;
中间组件
// 引入展示主题组件
import ThemeConsumerComp from './ThemeConsumerComp'
// 中间组件,不做任何操作,只为引入下一级组件做隔层传递数据展示
class IntermediaryComponents extends React.Component {
constructor(props) {
super(props);
}
render() {
const { theme } = this.props;
return <ThemeConsumerComp theme={theme} />
}
}
export default IntermediaryComponents;
展示组件
// 最底端展示主题的组件
class ThemeConsumerComp extends React.Component {
constructor(props) {
super(props);
}
render() {
const { theme } = this.props;
return (
<p>顶层组件定义的主题:颜色为{theme.color}, 大小为{theme.size}</p>
)
}
}
可以看到,在中间组件中,并没有使用到 theme ,但是为了能传递到展示主题的组件中,我们不得不在中间组件里面先接收 props,再把 props 里面的 theme 传递下去。
在上面的例子中,仅仅嵌套三层组件即便是显示的传递 theme 也并不麻烦。
不过请试想,当嵌套的层数达到五层,十层呢,再使用通过 props 一层一层的去传递,那不仅工作量会大大增加,也不易维护,当我们需要再新增一个变量时又得再次显示传递多层。
这就是为什么需要使用 Context:
Context 提供了一个无需为每层组件手动添加 props,就能在组件树间进行数据传递的方法。
2.何时应该使用 Context
Context 的主要应用场景在于多层级的各个组件获取相同的数据。
但是 Context 也需要谨慎使用,其可能导致组件的复用性变差。例如大量用到 context 传下来的值的组件和提供 context 的组件的耦合性会增加。
注:如果仅仅想避免层层传递一些属性,而这些属性又仅有一两个组件可能使用到,使用 组合组件 在有些时候会是个更好的解决办法。简单来说,就是直接把这个组件整个作为属性传递下去,需要用的属性直接带在这个组件上,那么虽然也需要层层显示传递。但此时就只需要传递一个组件而不用考虑大量的属性。
// 引入展示主题组件
import ThemeConsumerComp from './ThemeConsumerComp'
// 顶层组件,定义主题
class Context extends React.Component {
constructor(props) {
super(props);
this.state = {
theme: {
color: 'light', // light,dark
size: 'small', // small, middle, large
}
}
}
render() {
const { theme } = this.state;
return (
<IntermediaryComponents
themeComp={<ThemeConsumerComp theme={theme} />}
/>
)
}
}
export default Context;
如代码中展示的那样,直接把用到 theme 的组件 作为属性传递下去。中间过程的组件就不需要再考虑 theme 是否变化了
当然,一般来说,顶层的 theme 可能会在很多不同组件中使用到,Context 能使用“广播”的形式让所有消费组件访问到,也能访问到后续的数据更新**。
使用 context 的通用的场景包括管理当前的 locale,theme,或者一些缓存数据。
注: 消费组件,简单来说能获取到到 context 的值的组件。
3.相关 API 简介
3.1 React.createContext
const ThemeContext = React.createContext(defaultValue);
上面一行代码创建了一个 Context 对象。当某组件(A组件)使用 ThemeContext.Provider 时会提供一个 Context 环境,A组件下的所有消费组件都可以访问到该 provider 上的 value 值。
注1:只有当组件所处的树中没有匹配到 Provider 时,其 defaultValue 参数才会生效。即某组件创建了 Context 对象,但是并没有使用 provider 创建 context 环境。
注2:将 undefined 传递给 Provider 的 value 时,消费组件的 defaultValue 不会生效。
注3:当消费组件想读取 value 时,会匹配离自身最近的 Provider ,这种情况出现于多个的 context 提供的 Provider 嵌套时。
3.2 Context.Provider
<ThemeContext.Provider value={theme}>
<IntermediaryComponents />
</ThemeContext.Provider>
这里提到的 provider 是每个 Context 对象都会返回的一个 Provider React 组件,它允许消费组件订阅 context 的变化。
可以看到,provider 可以接受一个 value 属性,其消费组件可以获取到这个 value。
注1:当多个 provider 嵌套时,里层的provider 在数据相同情况下会覆盖外面的 provider。类似于作用域变量的获取。
注2:当 provider 的值发生变化时,它内部所有的消费组件都会重新渲染。Provider 及其内部 consumer 组件都不受制于 shouldComponentUpdate 函数,因此当 consumer 组件在其祖先组件退出更新的情况下也能更新。
注3:通过新旧值检测来确定变化,使用了与 Object.is 相同的算法。关于比较算法,可以查阅:
ES 标准中的相等比较算法 。
注4:provider 上的 value 的值最好交由 state 来管理,这是因为若是直接写成对象,在父组件渲染时value 的值每次都会被重新赋值,导致下面的消费组件也会跟着渲染。
3.3 Class.contextType
挂载在 class 组件上的 contextType 属性会被重赋值为一个由 React.createContext() 创建的 Context 对象。这能让你使用 this.context 来消费最近 Context 上的那个值。
你可以在任何生命周期中访问到它。
class ThemeConsumerComp extends React.Component {
render() {
return (
<div>
<p>顶层组件定义的主题:颜色为{this.context.color}, 大小为{this.context.size}</p>
</div>
)
}
}
ThemeConsumerComp.contextType = ThemeContext;
export default ThemeConsumerComp;
当然,你也可以使用 static 这个类属性来初始化你的 contextType。
class ThemeConsumerComp extends React.Component {
static contextType = ThemeContext;
render() {
return (
<div>
<p>顶层组件定义的主题:颜色为{this.context.color}, 大小为{this.context.size}</p>
</div>
)
}
}
export default ThemeConsumerComp;
3.4 Context.Consumer
在上面的 class.contextType 中给出了 class 组件可以怎么消费最近 Context 上的那个值。
那么函数组件并没有实例,也就没有 this ,该如何拿到 context 上的值呢,这就是即将说到的 Context.Consumer 组件。(class 组件也可以使用该方式)
Context.Consumer 需要一个函数作为子元素,该函数接收一个参数,即是当前最近 context 上的值,然后会返回一个 React 元素(通过 JSX 的方式利于书写)。
const ThemeConsumerComp = () => {
return (
<div>
<ThemeContext.Consumer>
{
(theme) => {
return (
<p>顶层组件定义的主题:颜色为{theme.color}, 大小为{theme.size}</p>
)
}
}
</ThemeContext.Consumer>
</div>
)
}
export default ThemeConsumerComp;
注:如果没有对应的 Provider,value 参数等同于传递给 createContext() 的 defaultValue。这一点可以与 3.1 节进行比照。
3.5 Context.displayName
context 对象接受一个名为 displayName 的 property,类型为字符串。实际上相当于起了一个别名,可以在使用 React DevTools 时来方便确认 context 要显示的内容。
ThemeContext.displayName = 'MyThemeDisplayName';
在 React DevTools 中:
<ThemeContext.Provider> // 展示"MyThemeDisplayName.Provider"
<ThemeContext.Consumer> // 展示"MyThemeDisplayName.Consumer"
4.完整示例
4.1 修改主题
1.themeContext.ts
抽出定义 context 对象的相关内容。
该部分实现了创建了一个 Context 对象。当某组件(A组件)使用 ThemeContext.Provider 时会提供一个 Context 环境,A组件下的所有消费组件都可以访问到该 provider 上的 value 值。
import React from 'react';
export const defalutValue = {
color: 'light', // light,dark
size: 'middle', // small, middle, large
};
// 通过 createContext 定义一个 context
export const ThemeContext = React.createContext(defalutValue);
2.context.tsx
通过 provider 可以添加一个 value 属性,其它消费组件可以获取到这个 value。
value 的值最好由 state 接管,可以避免父组件重新渲染时导致子组件不必要的渲染。(参考:3.2 Context.Provider 注4 )。
import React from 'react';
import { ThemeContext } from './themeContext';
import IntermediaryComponents from './IntermediaryComp'
/**
* 顶层组件,定义主题
*/
class Context extends React.Component {
constructor(props) {
super(props);
this.state = {
theme: {
color: 'light', // light,dark
size: 'middle', // small, middle, large
}
}
}
changeTheme = () => {
const { theme: { color } } = this.state;
const theme = {
color: color === 'light' ? 'dark' : 'light', // light,dark
size: color === 'light' ? 'large' : 'middle', // small, middle, large
}
this.setState({ theme });
}
render() {
const { theme } = this.state;
return (
<div>
{/* 使用定义的 context 的 provider 做父容器,value 是传递的值,
如果需要传多个值,可以用对象封装一下 */}
<ThemeContext.Provider value={theme}>
<IntermediaryComponents />
</ThemeContext.Provider>
<hr />
{/* 修改顶层组件的主题,就能全局实现修改主题 */}
<button onClick={this.changeTheme}>修改主题</button>
</div>
)
}
}
export default Context;
3.IntermediaryComponents.tsx
中间组件,不做任何操作,只为引入下一级组件做隔层传递数据展示
import React from 'react';
import ThemeConsumerComp from './ThemeConsumerComp'
class IntermediaryComponents extends React.Component {
render() {
return (
<div>
<ThemeConsumerComp />
</div>
)
}
}
export default IntermediaryComponents;
4.ThemeConsumerComponents.tsx
通过 Context.Consumer 使用一个函数作为子元素,该函数接收一个参数,即是当前最近 context 上的值,然后会返回一个 React 元素。
class 组件也可以使用该方式接受,或者使用 contextType 的方式(参考: 3.3 Class.contextType)
import React from 'react'
import { ThemeContext } from './themeContext';
class ThemeConsumerComp extends React.Component {
render() {
return (
<ThemeContext.Consumer>
{
(theme) => {
return (
<p>顶层组件定义的主题:颜色为{theme.color}, 大小为{theme.size}</p>
)
}
}
</ThemeContext.Consumer>
)
}
}
export default ThemeConsumerComp;
4.2 在嵌套组件中更新 Context
在实际工作中,修改主题的按钮可能存在于不同的地方,因此支持在消费组件中更新 context 是很有必要的。
为了实现该功能,可以在 context 中传递一个函数。
下面代码是在 4.1 示例中进行部分修改
1.themeContext.ts
保证代码健壮性,在 defalutValue 增加 toggleTheme。
import React from 'react';
export const defalutValue = {
color: 'light', // light,dark
size: 'middle', // small, middle, large
toggleTheme: () => {},
};
// 通过 createContext 定义一个 context
export const ThemeContext = React.createContext(defalutValue);
2.context.tsx
重点在于增加 this.toggleTheme ,并随着 theme 一起作为 provider 的 value 提供给消费组件。
import React from 'react';
import { ThemeContext } from './themeContext';
import IntermediaryComponents from './IntermediaryComp'
class Context extends React.Component {
constructor(props) {
super(props);
this.toggleTheme = () => {
this.setState(state => ({
theme: {
color: state.theme.color === 'light' ? 'dark' : 'light', // light,dark
size: state.theme.color === 'light' ? 'large' : 'middle', // small, middle, large
toggleTheme: this.toggleTheme,
}
}));
};
this.state = {
theme: {
color: 'light', // light,dark
size: 'middle', // small, middle, large
toggleTheme: this.toggleTheme,
}
}
}
render() {
const { theme } = this.state;
return (
<div>
{/* 使用定义的 context 的 provider 做父容器,value 是传递的值,
如果需要传多个值,可以用对象封装一下 */}
<ThemeContext.Provider value={theme}>
<IntermediaryComponents />
</ThemeContext.Provider>
<hr />
</div>
)
}
}
export default Context;
3.IntermediaryComponents.tsx
该部分代码与 示例 4.1 中一致。
4.ThemeConsumerComponents.tsx
调用 context 环境中的 theme.toggleTheme() 来实现主题的修改。
import React from 'react'
import { ThemeContext } from './themeContext';
class ThemeConsumerComp extends React.Component {
render() {
return (
<ThemeContext.Consumer>
{
(theme) => {
return (
<>
<p>顶层组件定义的主题:颜色为{theme.color}, 大小为{theme.size}</p>
<button onClick={() => { theme.toggleTheme(); }}>
在消费组件中更改主题按钮
</button>
</>
)
}
}
</ThemeContext.Consumer>
)
}
}
export default ThemeConsumerComp;
5.消费多个 context
为了确保 context 快速进行重渲染,React 需要使每一个 consumers 组件的 context 在组件树中成为一个单独的节点。
<ThemeContext.Provider value={theme}>
<UserContext.Provider value={signedInUser}>
<Layout />
</UserContext.Provider>
</ThemeContext.Provider>