0%

【React入门】Context

Context的概念比较容易理解,它的目的就是做组件间数据共享。让嵌套很深的组件不需要一层一层的把props传下来。
我们先看一下如何使用Context

创建Context

我们用React.createContext来创建Context对象。可以给他传默认值。我们就用React官网的例子

1
const MyContext = React.createContext(defaultValue);

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

如果他没有匹配到Provider也就是没有被Provider包裹的情况下。将使用defaultValue。

这有助于在不使用 Provider 包装组件的情况下对组件进行测试。注意:将 undefined 传递给 Provider 的 value 时,消费组件的 defaultValue 不会生效。

Context.Provider

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

每个Context对象都会返回一个Provider React组件,它允许Consumer订阅context的变化。
Provider接收一个value属性,传递给Consumer组件。一个Provider可以和多个Consumer组件有对应关系。多个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
20
21
class MyClass extends React.Component {
componentDidMount() {
let value = this.context;
/* 在组件挂载完成后,使用 MyContext 组件的值来执行一些有副作用的操作 */
}
componentDidUpdate() {
let value = this.context;
/* ... */
}
componentWillUnmount() {
let value = this.context;
/* ... */
}
render() {
let value = this.context;
/* 基于 MyContext 组件的值进行渲染 */
}

// 或者 static contextType = MyContext;
}
MyClass.contextType = MyContext;

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

Context.Consumer

用来订阅context的变更,它让函数式组件可以订阅context。
这种方法,子元素必须是一个函数,这个函数接收context的值,并返回一个React节点。传递给函数的value值等价于组件树上方离这个context最近的Provider提供的value值。如果没有对应的Provider,value参数等同于传递给createContext()的defaultValue。

Context.displayName

context 对象接受一个名为 displayName 的 property,类型为字符串。React DevTools 使用该字符串来确定 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>
);
}
}

对比新老Context

在我最初用Context的时候,官方还是不推荐使用的。说是一般程序没有必要用Context而且Context还在实验阶段将在之后的版本里删除。在新的Context出来之后,现在可以频繁的见到Context的使用。而且在hook出现之后,useContext和useReducer配合还可以代替Redux。

老Context存在的问题。

在16.3之前Context这样用。

1
2
3
4
5
6
7
8
9
class Parent extends React.Component {
getChildContext() {
return {color: "purple"};
}
}

Parent.childContextTypes = {
color: PropTypes.string
};

然后就可以在任意一级子组件上访问 Context 里的内容了:

1
2
3
4
5
6
7
8
9
10
11
12
13
class Child extends React.Component {
render() {
return (
<p>
{this.context.color}
</p>
);
}
}

Child.contextTypes = {
color: PropTypes.string
};

用起来也不太复杂。但是和shouldComponentUpdate搭配起来就有问题。

1
2
3
4
5
<a>
<b>
<c>
</b>
</a>

其中组件a会通过 getChildContext 设置 Context,组件c通过 this.context 读取 Context。
当a设置新的Context,会触发子组件rerender,然后b会rerender。最终导致c的rerender,并且在 c render的时候会取到新的context。
这一切都很正常,如果这是b中使用了shouldComponentUpdate的时候,由于b中的state和props没有变,不会导致b的rerender,导致c也不会rerender这时,c就取不到新的Context的值。

新的Context

  1. Provider 和 Consumer 必须来自同一次 React.createContext 调用。也就是说 A.Provider 和 B.Consumer 是无法搭配使用的。
  2. React.createContext 方法接收一个默认值作为参数。当 Consumer 外层没有对应的 Provider 时就会使用该默认值。
  3. Provider 组件的 value prop 值发生变更时,其内部组件树中对应的 Consumer 组件会接收到新值并重新执行 children 函数。此过程不受 shouldComponentUpdete 方法的影响。
  4. Provider 组件利用 Object.is 检测 value prop 的值是否有更新。注意 Object.is 和 === 的行为不完全相同。
  5. Consumer 组件接收一个函数作为 children prop 并利用该函数的返回值生成组件树的模式被称为 Render Props 模式。