React Context及换肤功能实现
# 前言
通过讲解 React Context 的用法,引出 React 换肤功能的实现
# Context 概念
在组件树中共享数据,避免逐层传递。
我们经常遇到这样的场景,数据需要传到子组件的子组件更甚至更下层组件,用props逐层传递的代码如下:
class App extends React.Component {
render() {
return <Toolbar theme="dark" />;
}
}
function Toolbar(props) {
// Toolbar 组件接受一个额外的“theme”属性,然后传递给 Button 组件。
// 如果应用中每一个单独的按钮都需要知道 theme 的值,这会是件很麻烦的事,
// 因为必须将这个值层层传递所有组件。
return (
<div>
<Button theme={props.theme} />
</div>
);
}
function Button(props){
// Button 组件根据传递过来的 theme 决定 背景色
return (
<button style={{backgroundColor:props.theme==='dark'?'black':'white'}}>
test
</button>
)
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
使用 context,可以避免中间组件传递props
# Context 基本使用
# Provider & Consumer
// 为当前的 theme 创建一个 context(“light”为默认值)。
const ThemeContext = React.createContext('light');
class App extends React.Component {
// 使用一个 Provider 来将当前的 theme 传递给以 Toolbar 开始的组件树
// 本例使用 "dark" 值覆盖默认的 "light"值
render() {
return (
<ThemeContext.Provider value="dark">
<Toolbar />
</ThemeContext.Provider>
)
}
}
function Toolbar() {
// 无需再传递 theme值
return (
<div>
<Button />
</div>
);
}
function Button(){
// 在 Context.Consumer 中通过 RenderProps 的方式使用
return (
<ThemeContext.Consumer>
{theme =>(
<button style={{backgroundColor:theme==='dark'?'black':'white'}}>
test
</button>
)}
</ThemeContext.Consumer>
)
}
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
30
31
32
33
Button 往上组件树寻找最近的 ThemeContext.Provider 提供的value值,如果没有对应的 Provider,使用 createContext 时的默认值
同时也说明了一个问题,数据是单向的自上而下,若 ThemeContext.Provider
定义在子组件, ThemeContext.Consumer
在父组件,子组件传的值传不到父组件中。这里就不举例了
# Class 组件使用 Consumer
在 Class 组件中也可以用 Consumer 的形式
class Button extends React.Component {
render() {
return (
<ThemeContext.Consumer>
{theme => (
<button style={{ backgroundColor: theme === 'dark' ? 'black' : 'white' }}>
test
</button>
)}
</ThemeContext.Consumer>
)
}
}
2
3
4
5
6
7
8
9
10
11
12
13
但是在 render 里这么写看着有点乱,如果 theme的值能像 props 那样使用就好了,
# Class 组件使用 contextType
这使用就要利用 Class.contextType
来获取 this.context
值,举个例子
class Button extends React.Component {
static contextType = ThemeContext;
render() {
let theme = this.context;
return (
<button style={{ backgroundColor: theme === 'dark' ? 'black' : 'white' }}>
test
</button>
)
}
}
2
3
4
5
6
7
8
9
10
11
static contextType = ThemeContext;
也可以写在外面:Button.contextType = ThemeContext;
挂载在 class 上的 contextType 属性会被重赋值为一个由 React.createContext() 创建的 Context 对象。这能让你使用 this.context 来消费最近 Context 上的那个值。你可以在任何生命周期中访问到它,包括 render 函数中。
参考:Class.contextType (opens new window)
# 消费多个 Context
可以看出来,如果是使用 contextType 的做法,只能消费一种 Context 且最近的那个 ,多 Context 还是得通过 Consumer 实现
举例:
const ThemeContext = React.createContext('light');
const UserContext = React.createContext({
name: 'Guest'
});
class App extends React.Component {
render() {
return (
<ThemeContext.Provider value="dark">
<ThemeContext.Provider value="blue">
<UserContext.Provider value={{ name: 'gahing' }}>
<Toolbar />
</UserContext.Provider>
</ThemeContext.Provider>
</ThemeContext.Provider>
);
}
}
function Toolbar() {
return (
<div>
<Button />
</div>
);
}
class Button extends React.Component {
static contextType = ThemeContext;
render() {
let theme = this.context;
console.log(theme)
return (
<button style={{ backgroundColor: theme === 'dark' ? 'black' : 'red' }}>
<UserContext.Consumer>
{(user) => (
<span>{user.name}</span>
)}
</UserContext.Consumer>
</button>
)
}
}
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
30
31
32
33
34
35
36
37
38
39
40
41
42
可以看到,theme 拿到的值为最近的 Provider 提供的 blue
,button里面的内容是 gahing
。
同时,要想使用 UserContext 值,需要使用 Context.Consumer
# 在嵌套组件中更新 Context
还是以最开始的 ThemeContext 为例,此时我们需要加个功能,点击 button 后 backgroundColor 会进行切换,
最简单的想法是就是 Provider 包组件的时候传一个 toggleTheme prop,然后一层层传上去,最后 button 点击的时候执行 toggleTheme 方法,
就又回到了最开始说 props 逐层传递的弊端,那应该怎么做呢?
把 toggleTheme 和 theme 都作为一个对象属性放在 React.createContext 中的默认值参数
export const ThemeContext = React.createContext({
theme: 'dark',
toggleTheme: () => {},
});
2
3
4
具体例子
const ThemeContext = React.createContext({
theme: 'dark',
toggleTheme: () => {},
});
class App extends React.Component {
constructor(props) {
super(props);
this.toggleTheme = () => {
this.setState((state) => ({
theme:
state.theme === 'dark'
? 'light'
: 'dark',
}));
};
this.state = {
theme: 'light',
};
}
render() {
return (
<ThemeContext.Provider value={{
theme:this.state.theme,
toggleTheme: this.toggleTheme,
}}>
<Toolbar />
</ThemeContext.Provider>
);
}
}
function Toolbar() {
return (
<div>
<Button />
</div>
);
}
class Button extends React.Component {
static contextType = ThemeContext;
render() {
let {theme, toggleTheme} = this.context;
return (
<button style={{ backgroundColor: theme === 'dark' ? 'black' : 'white' }} onClick={toggleTheme}>
test
</button>
)
}
}
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
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
效果即默认白色按钮,点击后切换成黑色,再点又变成白色...
# Context 注意事项
写例子的时候用的是 tsx,不了解 typescript 的可以直接把 any 之类的删去
# 1.当传递对象给 value 时,检测变化的方式会导致一些问题
上面的例子中, ThemeContext value的值是这样的
<ThemeContext.Provider value={{
theme:this.state.theme,
toggleTheme: this.toggleTheme,
}}>
2
3
4
当 provider 的父组件(App)进行重渲染(执行render方法)时,由于 provider 的value
属性总是一个新的对象,导致 consumers 组件会触发意外的渲染
把上面的例子稍微改造下就知道了
const ThemeContext = React.createContext({
theme: 'dark',
toggleTheme: () => {},
});
class App extends React.Component<any,any> {
private toggleTheme:any;
constructor(props: any) {
super(props);
this.toggleTheme = () => {
this.setState((state: any) => ({
themeContext: {
theme: state.themeContext.theme === 'dark'
? 'light'
: 'dark',
toggleTheme: state.themeContext.toggleTheme
}
}));
};
this.state = {
themeContext: {
theme: 'light',
toggleTheme: this.toggleTheme,
},
count: 1,
};
}
componentDidMount(){
setTimeout(() => {
this.setState({
count:2
})
}, 5000);
}
render() {
console.log('render App')
return (
<ThemeContext.Provider value={{
theme: this.state.themeContext.theme,
toggleTheme: this.toggleTheme,
}}>
<Toolbar />
<span>{this.state.count}</span>
</ThemeContext.Provider>
);
}
}
// 使用memo,当props没有变动时不触发render
const Toolbar = React.memo(()=> {
console.log('render Toolbar')
return (
<div>
<Button />
</div>
);
})
// 使用 PureComponent,本来应该是 props没有变动时不触发render,但本例中还是触发了render
class Button extends React.PureComponent {
static contextType = ThemeContext;
render() {
let {theme, toggleTheme} = this.context;
console.log('render Button')
return (
<button style={{ backgroundColor: theme === 'dark' ? 'black' : 'white' }} onClick={toggleTheme}>
test
</button>
)
}
}
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
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
输出结果为
render App
render Toolbar
render Button
# 自动过5s后输出
render App
render Button
2
3
4
5
6
可以发现,App组件重渲染的时候,Button 这个 consumers 组件也发生了重渲染
Button 换成 Consumer 的实现
class Button extends React.PureComponent {
render() {
console.log('render Button')
return (
<ThemeContext.Consumer>
{({ theme, toggleTheme }) => {
console.log('render Consumer')
return (
<button style={{ backgroundColor: theme === 'dark' ? 'black' : 'white' }} onClick={toggleTheme}>
test
</button>
)
}}
</ThemeContext.Consumer>
)
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
输出结果为
render App
render Toolbar
render Button
render Consumer
# 自动过5s后输出
render App
render Consumer
2
3
4
5
6
7
此时没有输出 render Button
是因为 Button 并不是 Consumer 组件,只有底下的子组件(Consumer包住的部分) 才是,才会进行重渲染
改造 value
,使得 consumers 组件不会重渲染
// App render 修改为
<ThemeContext.Provider value={this.state.themeContext}>
2
输出结果为
render App
render Toolbar
render Button
render Consumer
# 自动过5s后输出
render App
# 点击按钮后输出
render App
render Consumer
2
3
4
5
6
7
8
9
这就说明,App组件进行重渲染,只要 provider 提供的 value 值不变,其下的 consumers 组件就不会意外的重渲染
除了将 value 状态提升到父节点的 state 里,也可以利用 memoization
来实现
import memoize from "memoize-one";
class App extends React.Component<any, any> {
private toggleTheme: any;
constructor(props: any) {
super(props);
this.toggleTheme = () => {
this.setState((state: any) => ({
themeContext: {
theme: state.themeContext.theme === 'dark'
? 'light'
: 'dark',
}
}));
};
this.state = {
themeContext: {
theme: 'light',
},
count: 1,
};
}
componentDidMount() {
setTimeout(() => {
this.setState({
count: 2
})
}, 5000);
}
cacheThemeContext = memoize((theme)=>({
theme,
toggleTheme: this.toggleTheme
}))
render() {
console.log('render App')
return (
<ThemeContext.Provider value={this.cacheThemeContext(this.state.themeContext.theme)}>
<Toolbar />
</ThemeContext.Provider>
);
}
}
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
30
31
32
33
34
35
36
37
38
39
40
41
# 2.什么情况下不该用 Context
Context 主要应用场景在于很多不同层级的组件需要访问同样一些的数据
如果你只是想避免层层传递一些属性,组件组合(component composition) (opens new window)有时候是一个比 context 更好的解决方案
参考: 使用 Context 之前的考虑 (opens new window)
大致意思就是把最底下需要用到 props 的组件提到最上层来,将组件包成一个prop往下传递,
这里有个疑问,那不还是得每个组件写一次 prop 而且这些高层组件变得更复杂了。。
当然有的说法是减少了传递的props数量,对高层组件更容易把控等等。。
所以,使用 组件组合
还是 Context
个人觉得没有详细的界限
# 换肤
需求很简单,换个主题色。
React组件库主题设计 (opens new window) 的做法,主题色定义在js中,和上文一样,通过 Context 去设置或切换主题色
同样的,主题色定义在 css 中,context 值保存 className,切换 className 实现主题切换
(示例就不提供了,可以看上面的参考文献)