Context 提供了一种跨组件访问数据的方法。它无需在组件树间逐层传递属性,也可以方便的访问其他组件的数据
在经典的 React 应用中,数据是父组件通过 props 向子组件传递的。但是在某些特定场合,有些数据需要在各个组件之间共享。Context 为我们提供一种组件之间共享数据的方式,可以避免数据在组件树上逐层传递
使用 Context 的场合
Context 可以在组件树的组件之间共享“全局”数据。例如:登陆的用户信息,用户选择的主题、语言等等。下面的例子中,我们“手动”自上而下传递 theme 属性,用来设定 Button 的样式。
class App extends React.Component {
render() {
return <Toolbar theme=”dark”></Toolbar>;
}
}
function Toolbar(props) {
// The Toolbar component must take an extra “theme” prop
// and pass it to the ThemedButton. This can become painful
// if every single button in the app needs to know the theme
// because it would have to be passed through all components.
return (
<div>
<ThemedButton theme={props.theme}></ThemedButton>
</div>
);
}
class ThemedButton extends React.Component {
render() {
return <Button theme={this.props.theme}></Button>;
}
}
使用 Context , 我们可以避免通过多个中间组件传递 props
// Context lets us pass a value deep into the component tree
// without explicitly threading it through every component.
// Create a context for the current theme (with “light” as the default).
const ThemeContext = React.createContext(‘light’);
class App extends React.Component {
render() {
// Use a Provider to pass the current theme to the tree below.
// Any component can read it, no matter how deep it is.
// In this example, we’re passing “dark” as the current value.
return (
<ThemeContext.Provider value=”dark”>
<Toolbar></Toolbar>
</ThemeContext.Provider>
);
}
}
// A component in the middle doesn’t have to
// pass the theme down explicitly anymore.
function Toolbar(props) {
return (
<div>
<ThemedButton />
</div>
);
}
class ThemedButton extends React.Component {
// Assign a contextType to read the current theme context.
// React will find the closest theme Provider above and use its value.
// In this example, the current theme is “dark”.
static contextType = ThemeContext;
render() {
return <Button theme={this.context} />;
}
}
有时候,有些数据需要被很多组件访问,而且这些组件在组件树的不同层上。Context 可以使我们以“广播”的形式,在各个组件中共享数据的改变
Context 相关 API
React.createContext
const MyContext = React.createContext(defaultValue);
创建一个新的 Context 对象。当 React 渲染一个组件,且该组件注册了 Context 时,它将读取父组件中,距离该组件最近的 Provider 组件的 Context 值
defaultValue 只有 在“Consumer”组件找不到 Provider 组件时,才会被使用。
Context.Provider
<MyContext.Provider value={/* some value */}>
每个 Context 对象都携带一个名叫 Provider 的 React 组件。Provider 可以使得“Consumer”组件监听 context 的变更
通过向 Provider 的后代 Consumer 组件传递 value 的 prop,一个 Provider 可以与多个 Consumer 组件建立联系。
所有的后代 Consumer 组件在 Provider 的 value 属性更新后,都会被重新渲染。这个更新从 Provider 到其后代 Consumer 组件之间传播,但是并不会触发 shouldComponentUpdate 方法。所以即使 Consumer 组件的祖先组件没有更新,Consumer 组件也会更新
Context 使用与 Object.is 相同的算法来对比 value 的新、旧值,以判定其 value 是否被更新了
注意
当向 value 传递对象时,这种判定 value 是否改变的方式可能会引起问题。请参加.
Class.contextType
class MyClass extends React.Component {
componentDidMount() {
let value = this.context;
/* perform a side-effect at mount using the value of MyContext */
}
componentDidUpdate() {
let value = this.context;
/* … */
}
componentWillUnmount() {
let value = this.context;
/* … */
}
render() {
let value = this.context;
/* render something based on the value of MyContext */
}
}
MyClass.contextType = MyContext;
为 class 的 contextTpe 属性赋值一个 Context 对象后,我们可以通过 this.context 在组件的各个声明周期函数中获取到当前的 Context 对象的方法
注意:
通过这种方式,每个组件只能注册一个 context 对象。如果需要读取多个 context 的 value 值,参加 Consuming Multiple Contexts.
如果编码中使用了 ES 实验中的语法,那么可以使用类的静态 (static) 成员来初始化 contextTYpe. 代码如下:
class MyClass extends React.Component {
static contextType = MyContext;
render() {
let value = this.context;
/* render something based on the value */
}
}
Context.Consumer
<MyContext.Consumer>
{value => /* render something based on the context value */}
</MyContext.Consumer>
Consumer 是一个监听 context 变化的 React 组件。它使得我们可以在一个函数组件中,监听 contxt 的改变。
Consumer 组件要求其子元素为一个函数。该函数的参数接收当前的 context 的 value 值,要求返回一个 React 节点 (node) 传递给该函数的参数 value 等于距离此 Consumner 最近的外层 Provider 组件的 context 值。如果没有外层的 Provider 组件,则等于调用 createContext() 时传递的参数值(context 的默认值)。
注意
更多关于“子元素为一个函数”的信息,请参加 render props
栗子
在嵌套组件中更新 Context
开发中,我们经常需要在某些嵌套结构很深的组件上更新 context 的 value 值。此时,我们可以向下传递一个函数,用它来更新 context 的 value。代码如下:
theme-context.js
// Make sure the shape of the default value passed to
// createContext matches the shape that the consumers expect!
export const ThemeContext = React.createContext({
theme: themes.dark,
toggleTheme: () => {},
});
theme-toggler-button.js
import {ThemeContext} from ‘./theme-context’;
function ThemeTogglerButton() {
// The Theme Toggler Button receives not only the theme
// but also a toggleTheme function from the context
return (
<ThemeContext.Consumer>
{({theme, toggleTheme}) => (
<button
onClick={toggleTheme}
style={{backgroundColor: theme.background}}>
Toggle Theme
</button>
)}
</ThemeContext.Consumer>
);
}
export default ThemeTogglerButton;
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 also contains the updater function so it will
// be passed down into the context provider
this.state = {
theme: themes.light,
toggleTheme: this.toggleTheme,
};
}
render() {
// The entire state is passed to the provider
return (
<ThemeContext.Provider value={this.state}>
<Content />
</ThemeContext.Provider>
);
}
}
function Content() {
return (
<div>
<ThemeTogglerButton />
</div>
);
}
ReactDOM.render(<App />, document.root);
使用多个 Contexts
为了保持 React 的快速渲染,我们需要将每个 consumer 组件编写成一个独立的组件节点(node)
// Theme context, default to light theme
const ThemeContext = React.createContext(‘light’);
// Signed-in user context
const UserContext = React.createContext({
name: ‘Guest’,
});
class App extends React.Component {
render() {
const {signedInUser, theme} = this.props;
// App component that provides initial context values
return (
<ThemeContext.Provider value={theme}>
<UserContext.Provider value={signedInUser}>
<Layout />
</UserContext.Provider>
</ThemeContext.Provider>
);
}
}
function Layout() {
return (
<div>
<Sidebar />
<Content />
</div>
);
}
// A component may consume multiple contexts
function Content() {
return (
<ThemeContext.Consumer>
{theme => (
<UserContext.Consumer>
{user => (
<ProfilePage user={user} theme={theme} />
)}
</UserContext.Consumer>
)}
</ThemeContext.Consumer>
);
}
如果有两个以上的 context 经常一起使用,我们需要考虑创建一个 render prop component 一并提供两个 Context
注意
因为 context 使用引用标示符 (reference identity) 来判断何时需要重新渲染,所以有些情况下,当 provider 的父元素重新渲染时,会触发 consumer 的非内部渲染。例如下面代码,在每次 Provider 重新渲染时,会重新渲染所有的 consumer 组件。因为会一直创建一个新的对象赋值给 value(value 一直在变)
class App extends React.Component {
render() {
return (
<Provider value={{something: ‘something’}}>
<Toolbar />
</Provider>
);
}
}
为了避免这个问题,可以将 value 放在组件的 state 中
class App extends React.Component {
constructor(props) {
super(props);
this.state = {
value: {something: ‘something’},
};
}
render() {
return (
<Provider value={this.state.value}>
<Toolbar />
</Provider>
);
}
}