文档上说:
Context
提供了一种在组件之间共享此类值的方式,而不必显式地通过组件树的逐层传递props
。
文档上说:Context
设计目的是为了共享那些对于一个组件树而言是“全局”的数据,例如当前认证的用户、主题或首选语言。
文档上说:不要仅仅为了避免在几个层级下的组件传递props
而使用context
,它是被用于在多个层级的多个组件需要访问相同数据的情景。
官方文档最后说:请谨慎使用
总之看React
官方文档对Context
的态度着实是够纠结。目前Context
已经有了新的API
,react
16.3之前的 Context API
已经不建议使用。
16.3之前的 Context API
的不足
完整代码在这里
React
中父子组件可以通过 props
自顶向下的传递数据。但是当组件深度嵌套时,手动逐级传递 prop
不仅书写起来很繁琐同时还会为夹在中间的组件引入不必要的 prop
。这时只需要在外层组件上
通过添加 childContextTypes
和 getChildContext
,声明要传递给子组件的 Context
,React
会向下自动传递,子组件只要通过定义contextTypes
就可以获取上层传递的数据。
类似这样就可以定义一个声明了Context
的组件
class UserContext extends React.Component {
static childContextTypes = {
user: PropTypes.shape({
name: PropTypes.string.isRequired,
age: PropTypes.number.isRequired,
changeUserName: PropTypes.func.isRequired
}).isRequired
};
constructor() {
super(...arguments);
this.state = {
user: {
name: "xingmu",
age: 38,
changeUserName: this.changeUserName
}
};
}
getChildContext() {
return this.state;
}
changeUserName = () => {
console.log("修改姓名");
this.setState({
user: {
...this.state.user,
name: "daixingmu"
}
});
};
render() {
return this.props.children;
}
}
为了方便使用,添加一个名字为ContextHOC
的高阶组件。
export default class ContextHOC extends React.Component {
render() {
return (
<UserContext>
{this.props.children}
</UserContext>
);
}
}
现在来使用一下
export default class User extends React.Component {
static contextTypes = {
user: PropTypes.object,
};
render() {
return (
<div>
<div>
姓名:{this.context.user.name}
<button onClick={this.context.user.changeUserName}>修改姓名</button>
</div>
<div>年龄:{this.context.user.age}</div>
</div>
);
}
}
export default class UserWrapper extends React.Component {
render() {
return (
<div>
用户情况:
<User />
</div>
);
}
}
现在将UserWrapper
渲染到界面上就可以看到显示效果了。
我们来看下下这个小例子的组件结构。
<ContextHOC>
<UserContext>
<UserWrapper>
<User />
</UserWrapper>
</UserContext>
</ContextHOC>
其中组件 UserContext
会通过 getChildContext
设置 Context
,组件 User
通过 this.context
读取 UserContext
传递的值。
当点击按钮更新name
属性的时候,组件 UserContext
通过 setState
设置新的 Context
值同时触发组件UserWrapper
的渲染。然后User
的渲染被触发,并在自己的 render
方法中拿到更新后的Context
。
整个流程如官方文档一样正常。
现在我们将组件 UserWrapper
的继承从React.Component
改为React.PureComponent
。
不了解
React.PureComponent
的可以自行搜索
import React from "react";
import User from "./User";
export default class UserWrapper extends React.PureComponent {
render() {
return (
<div>
用户情况:
<User />
</div>
);
}
}
然后看下效果。
可以看到,点击之后,触发了修改name
的事件,可以界面上的name
属性并没有被修改。这是为什么的呢?
当点击按钮更新name
属性的时候,组件 UserContext
通过 setState
设置新的 Context
值同时触发组件UserWrapper
的渲染。
因为这时UserWrapper
组件继承了React.PureComponent
,自动执行了shouldComponetUpdate
检测到 state
与 props
均未变化因此返回 false
,无需重新渲染。
导致User
也没有被触发渲染。因此最新的 Context
值也就没有机会渲染到页面上。
使用hack
的方法绕过中间组件
通过上的分析,可以知道定义Context
的组件和使用Context
组件中间的组件,如果使用了React.PureComponent
或者重写了shouldComponetUpdate
的时候,可能会造成context
值传递不下的问题。所以我在想是不是可以考虑绕过中间组件,通过发布订阅,在数据更新的时候,直接来更新目标组件??
定义一个Context
内容的模板。
class ContextHack {
constructor(data = {}) {
this.data = data;
this.events = [];
this.subscribe = this.subscribe.bind(this);
}
subscribe(event) {
let e = event;
this.events.push(e);
return () => {
this.events = this.events.filter(item => {
return item !== e;
});
};
}
setData(data) {
this.data = {
...this.data,
...data
};
this.events.forEach(event => {
event && event();
});
}
}
export default ContextHack;
然后修改UserContext
组件
class UserContext extends React.Component {
static childContextTypes = {
user: childContextTypes.isRequired
};
constructor() {
super(...arguments);
this.contextHack = new ContextHack({
name: "xingmu",
age: 38,
changeUserName: this.changeUserName
});
}
getChildContext() {
return { user: this.contextHack };
}
changeUserName = () => {
console.log("修改姓名");
this.contextHack.setData({
name: "daixingmu"
});
};
render() {
return this.props.children;
}
}
然后修改 User
组件,这里代码不贴了,全部代码都在这里,做为对该问题思考的总结。
16.3新版API
新版 Context API
都由以下几部分组成:
React.createContext
方法用于创建一个Context
对象。该对象包含Provider
和Consumer
两个属性,分别为两个React
组件。Provider
组件。用在组件树中更外层的位置。它接受一个名为value
的prop
,其值可以是任何JavaScript
中的数据类型。Consumer
组件。可以在Provider
组件内部的任何一层使用。它接收一个函数做为children
。这个函数的参数是Provider
组件接收的那个value
的值,返回一个React
元素。
let ThemeContext = React.createContext({
style: {
color: "green",
background: "red",
}
});
function Button({ theme, ...props }) {
return (
<button {...props} >我是按钮</button>
);
}
function ThemedButton(props) {
return (
/*
* Consumer会自动获取离其最近的Provider提供的value
* */
<ThemeContext.Consumer>
{
(context) => {
return <Button {...props} style={context.style} />;
}
}
</ThemeContext.Consumer>
);
}
function Toolbar(props) {
return (
<ThemeContext.Provider value={{
style: {
color: "red",
background: "green",
}
}}>
<ThemedButton />
</ThemeContext.Provider>
);
}
ReactDOM.render(
<Toolbar />,
document.getElementById("root")
);
这版 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
模式。
通过高阶函数简化
import { ThemeContext } from "./themeContext";
import React from "react";
export function withThemeContext(Component) {
return function (props) {
return (
<ThemeContext.Consumer>
{
(context) => {
return <Component {...props} style={context.style} />;
}
}
</ThemeContext.Consumer>
);
};
}
16.6 提供了便利的API
为了简化 Context API
的使用,React 16.6
的版本新提供了一个contextType
来简化使用
import { ThemeContext } from "./themeContext";
import React from "react";
class Button extends React.Component {
static contextType = ThemeContext;
render() {
return (
<button onClick={this.context.change} style={this.context.style}>我是按钮3333</button>
);
}
}
export { Button };
通过这种方式,每个组件只能注册一个context对象。如果需要读取多个context的value值,参加Consuming Multiple Contexts.
hooks
再后来 hooks
提供了 useContext
的 API
可以在函数组件中更方便的使用任意多个Context
,感兴趣的可以去官方文档
,这里就不列举了。