react Context

1 有什么作用

使用 Context 可以在组件树中共享全局数据,如主题、语言等,而无需使用 props 将数据在组件树中层层传递。

2 使用方法

2.1 创建 Context

1
2
//当组件所在的树中没有匹配到 Provider 时,defaultValue 才生效。
const MyContext = React.createContext(defaultValue);

使用 React.createContext 来创建 Context。

  • Context 对象被创建后,可以被其他组件订阅
  • 订阅 Context 对象的组件被称作消费组件
  • 一个 Context 可以被多个消费组件订阅

2.2 使用 Provider 包裹消费组件

Context.Provider 属性是一个组件,使用 Context.Provider 包裹消费组件,能够将 Context 值的最新变化传递到消费组件,使消费组件被重新渲染

  • 消费组件从离自身最近的 Provider 中读取当前的 Context 值,没找到 Provider 时,会使用 Context 被创建时的默认值。
  • 多个 Provider 可以嵌套使用,里层的会覆盖外层的数据。
  • 当 Provider 的 value 值发生变化时,其内部的所有消费组件都会被重新渲染。
  • Provider 及其内部消费组件不受制于 shouldComponentUpdate 函数,因此消费组件在其祖先组件跳过更新的情况下也能更新。
1
2
//当 value 的值为 undefined 时,不会使用 Context 对象的 defaultValue 参数的值
<MyContext.Provider value={"somevalue"} />

2.2 订阅 Context

2.2.1 Context.Consumer

消组件费为函数组件时,使用 Context.Consumer 来包裹子组件,可以订阅 Context 对象。

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
import {ThemeContext} from './theme-context';

function ThemeTogglerButton() {
return (
/*使用
<Context.Consumer >
{ (value) => (
//在此处渲染函数组件内容
)}
</Context.Consumer >
包裹函数组件渲染内容
*/
<ThemeContext.Consumer>
{({theme, toggleTheme}) => (
<button
onClick={toggleTheme}
style={{backgroundColor: theme.background}}>

Toggle Theme
</button>
)}
</ThemeContext.Consumer>
);
}

export default ThemeTogglerButton;

2.2.2 contextType

消费组件为类组件时,通过将 Context 对象赋值给 Class.contextType 来订阅 Context 对象。此方法只能订阅单一的 Context 对象。

  • 将 Context 对象赋值给 Class.contextType 后,可以在任何生命周期中,使用 this.context 来获取订阅的 Context 对象的值。

  • 消费组件会使用离自己最近的 Context.Provider 的 value 值。

    1
    2
    3
    4
    5
    6
    7
    class MyClass extends React.Component {
    static contextType = MyContext;//订阅 Context 对象
    render() {
    let value = this.context;//使用 Context 对象的值
    /* 基于这个值进行渲染工作 */
    }
    }

2.2.3 订阅多个 Context

React 需要每个 Context 对象是一个单独的节点,以确保 Context 快速渲染。

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
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
// Theme context,默认的 theme 是 “light” 值
const ThemeContext = React.createContext('light');

// 用户登录 context
const UserContext = React.createContext({
name: 'Guest',
});

class App extends React.Component {
render() {
const {signedInUser, theme} = this.props;

// 提供初始 context 值的 App 组件
return (
<ThemeContext.Provider value={theme}>
<UserContext.Provider value={signedInUser}>
<Layout />
</UserContext.Provider>
</ThemeContext.Provider>
);
}
}

function Layout() {
return (
<div>
<Sidebar />
<Content />
</div>
);
}

// 一个组件可能会订阅多个 context
function Content() {
return (
<ThemeContext.Consumer>
{theme => (
<UserContext.Consumer>
{user => (
<ProfilePage user={user} theme={theme} />
)}
</UserContext.Consumer>
)}
</ThemeContext.Consumer>
);
}

2.3 更新 Context 方法

2.3.1 Provider

可以通过 Provider 的 value 属性来修改 Context 的值,其内部的消费者会被重新渲染。

1
2
//当 value 的值为 undefined 时,不会使用 Context 对象的 defaultValue 参数的值
<MyContext.Provider value={"somevalue"} />

2.3.2 消费组件

可以通过给 Context 传递一个函数,使得消费组件可以通过此函数来更新 Context。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
//theme-context.js
// 确保传递给 createContext 的默认值数据结构是调用的组件(consumers)所能匹配的!
export const ThemeContext = React.createContext({
theme: themes.dark,
toggleTheme: () => {} //传递函数 给消费组件调用
});
export const themes = {
light: {
foreground: '#000000',
background: '#eeeeee',
},
dark: {
foreground: '#ffffff',
background: '#222222',
},
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
//theme-toggler-button.js
import {ThemeContext} from './theme-context';

function ThemeTogglerButton() {
// Theme Toggler 按钮不仅仅只获取 theme 值,它也从 context 中获取到一个 toggleTheme 函数
return (
<ThemeContext.Consumer>
{({theme, toggleTheme}) => (
<button
onClick={toggleTheme}
style={{backgroundColor: theme.background}}>
Toggle Theme
</button>
)}
</ThemeContext.Consumer>
);
}

export default ThemeTogglerButton;
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
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
//app.js
import {ThemeContext, themes} from './theme-context';
import ThemeTogglerButton from './theme-toggler-button';

class App extends React.Component {
constructor(props) {
super(props);

this.toggleTheme = () => {
this.setState(state => ({
theme:
state.theme === themes.dark
? themes.light
: themes.dark,
}));
};

// State 也包含了更新函数,因此它会被传递进 context provider。
this.state = {
theme: themes.light,
toggleTheme: this.toggleTheme,
};
}

render() {
// 整个 state 都被传递进 provider
return (
<ThemeContext.Provider value={this.state}>
<Content />
</ThemeContext.Provider>
);
}
}

function Content() {
return (
<div>
<ThemeTogglerButton />
</div>
);
}

ReactDOM.render(<App />, document.root);

2.4 调试

React DevTools 使用 Context.displayName 来确定 context 要显示的内容。

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

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

3 注意

  1. Context 会影响性能,不要滥用。
  2. 每个 Provider 提供的数据都是隔离开的,如果需要提供全局的数据,只需在 React 根部组件只提供一个 Provider 即可。
  3. 为了避免不必要的重复渲染,应使用 state 来配置 Provider 组件的 value 属性。
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
//Provider每次渲染时,以下的代码会重新渲染内部所有的consumers 组件
class App extends React.Component {
render() {
return (
<Provider value={{something: 'something'}}>
<Toolbar />
</Provider>
);
}
}

//应按照如下方法,使用 state 来更新 Provider 的 value 属性
class App extends React.Component {
constructor(props) {
super(props);
this.state = {
value: {something: 'something'},
};
}

render() {
return (
<Provider value={this.state.value}>
<Toolbar />
</Provider>
);
}
}