Context 设计目的是为共享那些被认为对于一个组件树而言是“全局”的数据,例如当前认证的用户、主题或首选语言。官方文档特别提醒说不要仅仅为了避免在几个层级下的组件传递 props 而使用 context,它是被用于在多个层级的多个组件需要访问相同数据的情景。
16.3之前的 Context API 的使用场景以及缺陷
该版本已经不建议使用
我们都知道在 React 中父子组件可以通过 props 自顶向下的传递数据。但是当组件深度嵌套时,从顶层组件向最内层组件传递数据就不那么方便了。手动在每一层组件上逐级传递 prop 不仅书写起来很繁琐同时还会为夹在中间的组件引入不必要的 prop。这时 Context API 就派上用场了。你只需要在外层组件上声明要传递给子组件的 Context:
注意:由于React v15.5开始 React.PropTypes已经废弃,现在需要单独使用prop-types来定义contextTypes。
通过在 Parent 上添加 childContextTypes 和 getChildContext , React会向下自动传递参数,任何组件只要在它的子组件中就能通过定义contextTypes来获取参数。
代码清单11
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16class Parent extends React.Component {
getChildContext() {
return {color: "purple"};
}
render() {
return (
<div>
<Child text={message.text} />
</div>
);
}
}
Parent.childContextTypes = {
color: PropTypes.string
};
然后就可以在任意一级子组件上访问 Context 里的内容了:
代码清单21
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
30import PropTypes from "prop-types";
import React from "react";
class Child extends React.Component {
render() {
return (
<button style={{background: this.context.color}}>
我是按钮
</button>
);
}
}
Child.contextTypes = {
color: PropTypes.string
};
// 上面的代码也可以通过无状态组件实现
import PropTypes from "prop-types";
import React from "react";
const Button = ({children}, context) =>(
<button style={{background: context.color}}>
我是按钮
</button>
);
Child.contextTypes = {
color: PropTypes.string
};
这个版本的 Context API
和 shouldComponentUpdate 搭配使用时很容易出问题。让我们通过一个小例子来简单说明,假设有以下组件结构:1
2
3
4
5<A>
<B>
<C />
</B>
</A>
其中组件 A 会通过 getChildContext
设置 Context,组件 C 通过 this.context 读取 Context。
当组件 A 要更新 Context 的时候发生什么呢?
- 组件 A 通过 setState 设置新的 Context 值同时触发子组件的 rerender。
- 组件 B rerender。
- 组件 C rerender,并在自己的 render 方法中拿到更新后的Context。
整个流程看起来好像没什么问题。如果我们在组件 B 上定义了 shouldComponentUpdate 会发生什么呢?
- 组件 A 通过 setState 设置新的 Context 值同时触发子组件的 rerender。
- 组件 B 执行 shouldComponetUpdate,由于组件 B 自身并不依赖 Context,所以 shouldComponetUpdate 检测到 state 与 prop 均未变化因此返回 false。无需重新 render。
- 由于 B 组件没有 rerender。这导致组件 C 也不会rerender,因此也就无法获取到最新的 Context 值。
- 由于 shouldComponentUpdate 是一个 React 开发人员经常使用的优化方法。所以如果代码里使用了这一版的
Context API
很大概率会遇到上述问题。
PropTypes 属性验证在打包为生产环境的时候一般会选择除去,这种使用方式也会产生选择的困难。
16.3新版Context API
新版 Context API 都由以下几部分组成:
- React.createContext 方法用于创建一个 Context 对象。该对象包含 Provider 和 Consumer两个属性,分别为两个 React 组件。
- Provider 组件。用在组件树中更外层的位置。它接受一个名为 value 的 prop,其值可以是任何 JavaScript 中的数据类型。
- Consumer 组件。可以在 Provider 组件内部的任何一层使用。它接收一个名为 children 值为一个函数的 prop。这个函数的参数是 Provider 组件接收的那个 value prop 的值,返回值是一个 React 元素(一段 JSX 代码)。
1 | let ThemeContext = React.createContext({ |
这版 Context API
的几个特点:
- Provider 和 Consumer 必须来自同一次
React.createContext
调用。也就是说NameContext.Provider
和AgeContext.Consumer
是无法搭配使用的。 React.createContext
方法接收一个默认值作为参数。当 Consumer 外层没有对应的 Provider 时就会使用该默认值。- Provider 组件的 value prop 值发生变更时,其内部组件树中对应的 Consumer 组件会接收到新值并重新执行 children 函数。此过程不受
shouldComponentUpdete
方法的影响。 - Provider 组件利用 Object.is 检测 value prop 的值是否有更新。注意
Object.is
和 === 的行为不完全相同。具体细节请参考Object.is
的 MDN 文档页。 - Consumer 组件接收一个函数作为 children prop 并利用该函数的返回值生成组件树的模式被称为
Render Props
模式。
注意: 因为 context 使用引用标示符(reference identity)来判断何时需要重新渲染,所以有些情况下,当 provider 的父元素重新渲染时,会触发 consumer 的非内部渲染。为了避免这个问题,可以将 value 放在 render 的外部,如 state
通过高阶函数简化
1 | import { ThemeContext } from "./themeContext"; |
16.6 提供了便利的API
为了简化 Context API的使用,React 16.6的版本新提供了一个contextType
来简化使用
1 | import { ThemeContext } from "./themeContext"; |
通过这种方式,每个组件只能注册一个context对象。如果需要读取多个context的value值,参加Consuming Multiple Contexts.