Context
在使用React时,很容易在自定义的React组件之间跟踪数据流。当监控一个组件时,可以监控到那些props被传递进入组件了,这非常有利于了解数据流在什么地方出现了问题。
在某些情况下,开发者想要通过组件树直接传递数据,而不是在一层又一层的组件之间手工传递数据。此时,可以使用React的“context”特性接口来快速实现这个功能。
尽量不要使用Context
React在16.x版本之后算是将Context调整为正式接口,不过还是建议如果组件之间传递数据的层次不算太深,尽量不要使用Context。而且 Redux 或 MobX 等技术能提供比Context特性更为优雅的实现方式。
最新实现方式
Context功能在16.x之后所有的API和使用方法都发生了巨大的改变,如果你使用的是最新版本(16.x)看这里最新方式就够了,如果是较早的版本,请看下方的历史实现小节。
新版本的Context实现方式简洁清晰许多,方式还是以类似于 高阶组件 包裹的方式为主。
入门使用案例
这是一个没有使用Context特性3个组件组合的使用例子:
class App extends React.Component {
render() {
return <Toolbar theme="dark" />;
}
}
function Toolbar(props) {
//为了让子组件能获取必要的参数,这里需要使用props.theme继续向子组件传递参数,
//但是实际上theme参数对于Toolbal组件来说并没有任何价值。
//例如项目全局设置了一个theme参数来控制很多组件的主题样式,
//那么这个参数需要在几乎所有的组件出现,并且不断的传递他
return (
<div>
<ThemedButton theme={props.theme} />
</div>
);
}
function ThemedButton(props) {
return <Button theme={props.theme} />;
}
上面的theme参数表示全局主题样式,很多组件通过他来控制自己当前应该呈现的样式。如果我们在根组件控制这个参数,那么几乎所有的组件都要向下传递这个参数。
下面是用Context特性实现的方式:
// 创建一个Context组件,可以理解为一种特殊的高阶组件。
// 'light'是当前的默认值
const ThemeContext = React.createContext('light');
class App extends React.Component {
render() {
//使用Provider将子组件包裹起来。
//将值修改为'dark'
return (
<ThemeContext.Provider value="dark">
<Toolbar />
</ThemeContext.Provider>
);
}
}
//中间组件,并不关心和他无关的参数
function Toolbar(props) {
return (
<div>
<ThemedButton />
</div>
);
}
//使用参数的组件
function ThemedButton(props) {
// 使用Consumer组件包裹需要获取参数的组件
// 在这个案例中,命名为light的Context被赋值"dark",然后在Consumer中获取这个值
return (
<ThemeContext.Consumer>
{theme => <Button {...props} theme={theme} />}
</ThemeContext.Consumer>
);
}
上面就是简单使用Context的例子,16.x之后也是通过高阶组件的方式来实现,是不是看了之后感觉很想Redux。只要是通过 Provider 包裹的组件,在其后的整个组件树中都可以用 Consumer 来获取指定的数据。
上面的代码我们也可以修改为下面这样更直观的形式:
const {Provider, Consumer} = React.createContext('light');
class App extends React.Component {
render() {
return (
<Provider value="dark">
<Toolbar />
</Provider>
);
}
}
function Toolbar(props) {
return (
<div>
<ThemedButton />
</div>
);
}
function ThemedButton(props) {
return (
<Consumer>
{theme => <Button {...props} theme={theme} />}
</Consumer>
);
}
使用Context需要注意:
- 由于 Provider 和 Consumer都是返回一个组件,所以我们最好设定一个默认的context.value,以防止出现渲染错误。
- 当Provider发生数据变更时,会触发到 Consumer 发生渲染,所有被其包裹的子组件都会发生渲染(render方法被调用)。
任意组件更新Context
某些时候需要在内部组件需要去更新Context的数据,其实我们仅仅需要向上下文增加一个回调即可,看下面的例子:
//创建Context组件
const ThemeContext = React.createContext({
theme: 'dark',
toggle: () => {}, //向上下文设定一个回调方法
});
function Button() {
return (
<ThemeContext.Consumer>
{({theme, toggle}) => (
<button
onClick={toggle} //调用回调
style={{backgroundColor: theme}}>
Toggle Theme
</button>
)}
</ThemeContext.Consumer>
);
}
//中间组件
function Content() {
return (
<div>
<Button />
</div>
);
}
//运行APP
class App extends React.Component {
constructor(props) {
super(props);
this.toggle = () => { //设定toggle方法,会作为context参数传递
this.setState(state => ({
theme:
state.theme === themes.dark
? themes.light
: themes.dark,
}));
};
this.state = {
theme: themes.light,
toggle: this.toggle,
};
}
render() {
return (
<ThemeContext.Provider value={this.state}> //state包含了toggle方法
<Content />
</ThemeContext.Provider>
);
}
}
App组件创建了Provider,并向其参数传递了一个回调方法,之后任何使用了 Consumer 的子孙组件都可以使用这个回调方法了触发更新。
多个Context复合使用
React支持设置多个Context,看下面的例子:
const ThemeContext = React.createContext('light'),
UserContext = React.createContext({
name: 'Guest',
});
class App extends React.Component {
render() {
const {signedInUser, theme} = this.props;
return (
<ThemeContext.Provider value={theme}>
<UserContext.Provider value={signedInUser}>
<Layout />
</UserContext.Provider>
</ThemeContext.Provider>
);
}
}
function Layout() {
return (
<div>
<Sidebar />
<Content />
</div>
);
}
function Content() {
return (
<ThemeContext.Consumer>
{theme => (
<UserContext.Consumer>
{user => (
<ProfilePage user={user} theme={theme} />
)}
</UserContext.Consumer>
)}
</ThemeContext.Consumer>
);
}
和使用单个Context也没多大区别,相互包装一层即可。
使用Context时需要牢记一点:和Redux一样,只要 Provider 的value发生变更都会触发所有 Consumer 包裹的子组件渲染。
16.x之后的Context使用起来比旧版本的简单明了太多,实现思路上还是学习了Redux等将状态抽取出来统一管理并触发更新的方式来实现,在使用时选择一种方式来实现就行。
历史实现
如何使用Context
假设有下面这样一个组件结构:
class Button extends React.Component {
render() {
return (
<button style={{background: this.props.color}}>
{this.props.children}
</button>
);
}
}
class Message extends React.Component {
render() {
return (
<div>
{this.props.text} <Button color={this.props.color}>Delete</Button>
</div>
);
}
}
class MessageList extends React.Component {
render() {
const color = "purple";
const children = this.props.messages.map((message) =>
<Message text={message.text} color={color} />
);
return <div>{children}</div>;
}
}
在上面的例子中,在最外层组件手工传入一个color
属性参数来指定Button
组件的颜色。如果使用Context特性,我们可以直接将属性自动的传递给整个组件树:
const PropTypes = require('prop-types');
class Button extends React.Component {
render() {
// 注意this.context.color
return (
<button style={{background: this.context.color}}>
{this.props.children}
</button>
);
}
}
// 限定color属性只接收string类型的参数
Button.contextTypes = {
color: PropTypes.string
};
class Message extends React.Component {
render() {
return (
<div>
{this.props.text} <Button>Delete</Button>
</div>
);
}
}
class MessageList extends React.Component {
// 在后续组件中设定一个Context的值
getChildContext() {
return {color: "purple"};
}
render() {
const children = this.props.messages.map((message) =>
<Message text={message.text} />
);
return <div>{children}</div>;
}
}
//限定子组件的color值只接收string类型的参数
MessageList.childContextTypes = {
color: PropTypes.string
};
通过在 MessageList
组件(Context的制定者)中增加 childContextTypes
和 getChildContext
,React会自动将这个指定的context值传递到所有子组件中(比如例子中的 Button
组件),而子组件也可以定义一个 contextTypes
来指定接收context的内容。如果未定义子组件的 contextTypes
,那么调用 context
只能得到一个空对象。
父子组件耦合
Context特性还可以让开发人员快速构建父组件与子组件之间的联系。例如在 React Router V4 包中:
import { BrowserRouter as Router, Route, Link } from 'react-router-dom';
const BasicExample = () => (
<Router>
<div>
<ul>
<li><Link to="/">Home</Link></li>
<li><Link to="/about">About</Link></li>
<li><Link to="/topics">Topics</Link></li>
</ul>
<hr />
<Route exact path="/" component={Home} />
<Route path="/about" component={About} />
<Route path="/topics" component={Topics} />
</div>
</Router>
);
例子通过Router组件传递一些数据,每一个被Router
包含的 Link
和 Route
都可以直接通信。但是建议在使用这些API构建组件时,先思考是否还有其他更清晰的实现方式。例如可以使用回调的方式去组合组件。
在生命周期方法中引入Context
如果在某个组件上定义了 contextTypes
,下面这些生命周期方法将会接收到额外的参数—— context
对象。我们这里这样调整参数接口:
constructor(props, context)
componentWillReceiveProps(nextProps, nextContext)
shouldComponentUpdate(nextProps, nextState, nextContext)
componentWillUpdate(nextProps, nextState, nextContext)
componentDidUpdate(prevProps, prevState, prevContext)
在无状态的方法性组件中引入Context
无状态的方法性组件也可以引入Context,前提是给组件定义了 contextTypes
。下面的代码展示了在无状态的组件—— Button
中引入context的表达式:
const PropTypes = require('prop-types');
const Button = ({children}, context) =>
<button style={{background: context.color}}>
{children}
</button>;
Button.contextTypes = {color: PropTypes.string};
更新Context
首先,千万不要更新Context。
React提供一个更新Context的接口,但是它会从根本上破坏React的结构所以建议不要使用他。
getChildContext
在state或props变更时会被调用。为了更新context中的数据可以使用 this.setState
方法来触发变更,触发之后context的更新会被子组件接收到。
const PropTypes = require('prop-types');
class MediaQuery extends React.Component {
constructor(props) {
super(props);
this.state = {type:'desktop'};
}
getChildContext() {
return {type: this.state.type};
}
componentDidMount() {
const checkMediaQuery = () => {
const type = window.matchMedia("(min-width: 1025px)").matches ? 'desktop' : 'mobile';
if (type !== this.state.type) {
this.setState({type});
}
};
window.addEventListener('resize', checkMediaQuery);
checkMediaQuery();
}
render() {
return this.props.children;
}
}
MediaQuery.childContextTypes = {
type: PropTypes.string
};
这里的问题在于,如果一个context在组件变更时才产生,接下来如果中间某个组件的 shouldComponentUpdate
方法返回fasle值,那么后续组件无法从context中得到任何值。所以,如果使用context来维护管理状态,那么就需要从全局去控制组件,这和React单向数据流和组件化的思路有些背道而驰。而且随着应用的扩展以及人员的更变,全局管理状态会越来越难。如果你还想了解更多关于context的问题,可以阅读这篇博客文章——“How To Safely
Use React Context"(翻墙),里面讨论了如果绕开这些问题。