前言

之前自己写demo项目的时候用到了React16新出的Context方法,确实是个好东西!避免忘记特此记录。
其实官网上对Context的讲解已经很详细了,接下来就记录一下我自己对Context的理解和使用;
好记性不如烂笔头,我相信这是一个好习惯~

产生

react组件之间(父传子)使用props来传递数据,子组件里面通过this.props就能拿到父级传来的值。一旦组件深度嵌套,数据又在顶级父级的身上,最末级组件用这些数据就要传递很多层,而且还有可能这些数据中间组件是完全用不到的。项目复杂度一上来,需要传的数据量也会越来越大,可想而知是多么的繁琐,子组件在调试数据时,找数据的来源也是个麻烦事!

1
2
3
4
5
6
7
8
9
10
11
12
13
// page 把 user avatarSize 传给PageLayout
<Page user={user} avatarSize={avatarSize} />

// PageLayout 再把 user avatarSize 传给NavigationBar
<PageLayout user={user} avatarSize={avatarSize} />

// NavigationBar 再传给 内部Avatar组件
<NavigationBar user={user} avatarSize={avatarSize} />

// Avatar 组件里面才真正使用 user avatarSize !
<Link href={user.permalink}>
<Avatar user={user} size={avatarSize} />
</Link>

为了避免这种繁琐的手动传递数据,Context方法应运而生。此处引用官方文档原话:“Context 提供了一个无需为每层组件手动添加 props,就能在组件树间进行数据传递的方法。”。

Context

React.createContext创建出一个Context对象,这个对象提供了四个api:

  • Context.Provider
  • Context.Consumer
  • Class.contextType
  • Context.displayName

React.createContext 创建

创建一个上下文的对象Context,包含两个组件:生产者(Provider)消费者(Consumer); defaultValue可以设置共享的默认数据

1
const MyContext = React.createContext(defaultValue);

Context.Provider 生产者

Provider将数据通过value提供给Consumer(消费者);

1
2
3
<MyContext.Provider value={/* 需要提供给子组件的数据 */}>
{/* 这里放消费组件 */}
</MyContext.Provider>

Context.Consumer 消费者

用消费者组件包裹子组件,通过函数传参的方式,把共享的数据提供给子组件;value就是共享的数据;

1
2
3
<MyContext.Consumer>
{value => /* 子组件*/}
</MyContext.Consumer>

Class.contextType

子组件被消费者包裹后,就可以把 MyContext 通过contextType方法,将数据注入组件进行使用了,而且数据是动态改变的,父级的数据改变会自动通知使用过数据的子组件更新数据;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
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 组件的值进行渲染 */
}
}
MyClass.contextType = MyContext;

注意:官方提供了两个在子组件拿到数据的方法;
(1)创建MyContext,父级用MyContext.Provider包裹,子组件通过Component.contextType = MyContext注入数据。 但是经过自己实际测试,这样拿到的数据是死了,如果父级把数据改变了是不会通知子组件更新数据的;
(2)想要数据更新,需要Provider套Consumer,然后在子组件使用Component.contextType = MyContext才能在组件内部拿到this.context且数据是动态更新的。

Context.displayName

这个可以理解为给context 在 React DevTools 中改名字用的,没有什么实际的作用;

1
2
3
4
5
const MyContext = React.createContext(/* some value */);
MyContext.displayName = 'MyDisplayName';

<MyContext.Provider> // "MyDisplayName.Provider" 在 DevTools 中
<MyContext.Consumer> // "MyDisplayName.Consumer" 在 DevTools 中

总结

  • 区别于props一层一层传参,Context可以支持跨层级组件通信;
  • Context API的使用基于生产者消费者模式;
  • 开发时需要考虑可控可复用性,在不破坏组件树依赖关系、影响范围小的情况可以使用Context,影响范围大还是需要借助reduxmobx等工具;

在demo项目中的使用记录

为了练习ConText, 用来做了项目的换肤功能,配色有点丑(毕竟不是专业UI😅),咱暂且就看功能点,先看一下效果:


theme.context.tsx(Create)

1
2
3
4
import * as React from "react";

// ThemeContext 为 Context对象,theme-gray是defaultValue用来共享的默认数据
export const ThemeContext = React.createContext('theme-gray');

ThemeContext.Provider 把Layout组件包裹起来,把需要共享的数据通过value属性提供进Layout组件中;

theme.tsx (Provider)

项目使用了mobx,数据存在store中;

1
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
import * as React from "react";
import { inject, observer } from "mobx-react";
import { withRouter } from "react-router";
import { ThemeContext } from './theme.context';
import Layout from '../layout';

@inject('LayoutStore')
@observer
class Theme extends React.PureComponent<IProps, any> {
readonly store: any = null;

constructor(props: any) {
super(props);
this.store = this.props.LayoutStore;
}

render() {
// currentTheme 为主题名
const { currentTheme } = this.store;
return (
<ThemeContext.Provider value={currentTheme}>
<Layout />
</ThemeContext.Provider>
)
}
}

export default withRouter(Theme);

layout.tsx (Consumer)

Layout组件里调用Consumer来使用context中共享的数据;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import { ThemeContext } from './theme/theme.context';

class Layout extends React.Component<any, any>{

render() {
return (
<ThemeContext.Consumer>
{
theme => (
<div className={theme}>
{()=> console.log(theme) /* 主题名 */}
</div>
)
}
</ThemeContext.Consumer>
)
}
}