Context
引言
context提供了一种数据传输方式,它使得数据可以直接通过组件树传递而不需要在每一个层级上手动地传递props。
在典型的React应用中,数据是通过props自上而下(父组件传递给子组件)传递的,但是对于同时被许多组件所需要的某些props(如个人偏好,UI主题)来说,使用这种方式传递数据简直就是受刑。Context提供了不需要显式地在组件树上每个层级传递prop而是直接在组件之间传递的方法。
什么时候使用context
context设计的目的是为了共享那些对于组件树而言是“全局”的数据,比如当前用户信息,主题或语言等。在下面的示例代码中,我们手动传递了一个“theme”prop
来为Button组件提供样式。
class App extends React.Component {
render() {
return <Toolbar theme="dark" />;
}
}
function Toolbar(props) {
//Toolbar组件必须要传递一个额外
//的prop“theme”给ThemedButton组件。
//如果应用中的每个按钮都需要知道theme是
//什么的话,那么这会要人老命的,因为你需要在所有
//组件中一个个传递。
return (
<div>
<ThemedButton theme={props.theme} />
</div>
);
}
class ThemedButton extends React.Component {
render() {
return <Button theme={this.props.theme} />;
}
}
但是使用contex的话,我们可以避免通过中间组件来传递props:
//context让我们不需要在每一个组件中显式地传递prop就
//可以之间将数据传递进位于组件树深层次的组件中。
//创建一个表示当前主题的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>
);
}
class ThemedButton extends React.Component {
//将当前主题的contex值赋值给contextType
//React会在当前层级之上找到最近的Provider并获取
//它的值。在本例中,当前主题是”dark"。
static contextType = ThemeContext;
render() {
return <Button theme={this.context} />;
}
}
使用context之前的考虑
context主要在位于不同嵌套层级的组件需要获取同一个数据是使用。请谨慎地使用它因为context会使你的组件复用度变差。
如果你仅仅只想避免在过多的层级传递prop,那么组件组合是比context更简单的解决方案。
比如在下面的例子中,Page
组件传递了user
和avatarSize
给了几个层级之下的Link
和Avatar
组件:
<Page user={user} avatarSize={avatarSize} />
// ... 它渲染了 ...
<PageLayout user={user} avatarSize={avatarSize} />
// ... 它渲染了 ...
<NavigationBar user={user} avatarSize={avatarSize} />
// ... 它渲染了 ...
<Link href={user.permalink}>
<Avatar user={user} size={avatarSize} />
</Link>
如果最后仅仅只有Avatar
组件使用了user
和avatarSize
,那么你可能会觉得把它们传递那么多层级完全没必要。如果Avatar组件需要从顶层传递更多的prop,那么你可能会因此抓狂,因为你需要同时在所有的中间组件上都添加这些prop一遍。
不适用context解决这个问题的方法是把Avatar
组件作为prop
传递下去,这样中间组件就不需要知道其他关于user或avatarSize的信息了:
function Page(props) {
const user = props.user;
const userLink = (
<Link href={user.permalink}>
<Avatar user={user} size={props.avatarSize} />
</Link>
);
return <PageLayout userLink={userLink} />;
}
// 现在我们将看到:
<Page user={user} avatarSize={avatarSize} />
// ... 它渲染了 ...
<PageLayout userLink={...} />
// ... 它渲染了 ...
<NavigationBar userLink={...} />
// ... 它渲染了 ...
{props.userLink}
现在,只有最顶层的Page
组件需要知道Link
组件和Avatar
组件需要使用user
和avatarSize
。
这对组件的控制反转通过减少传递的prop的数量以及对跟组件的更多控制使你的代码更加简洁。但是这并不适用于所有情况,让复杂的逻辑位于高层级组件会使得它们变得复杂并且强制让低层级组件适应这种情况可能不是你想要的。
你的组件并不限制于只能接收一个子组件,你可以在组件中传递多个子组件,甚至为子组件封装多个插槽(slots),正如文档中所举例的:
function Page(props) {
const user = props.user;
const content = <Feed user={user} />;
const topBar = (
<NavigationBar>
<Link href={user.permalink}>
<Avatar user={user} size={props.avatarSize} />
</Link>
</NavigationBar>
);
return (
<PageLayout
topBar={topBar}
content={content}
/>
);
}
上述模式适用于大部分场景,在这些场景下你需要将子组件和直接父母组件解耦。如果子组件在渲染之前组要和父组件交互,你可以在这篇文章中获取相关知识。
然后有些时候某一数据被在组件树不同嵌套层级的组件所需要。context能够让你“广播”这些数据,所以在这种情况下请直接使用context。使用context的场景通常是管理locale,theme和一些缓存数据,这比使用替代方案简单的多。
API
React.createContext
const MyContext = React.createContext(defaultValue);
上述代码创建了一个context对象。当React渲染的组件使用了这个context对象时,React会从当前层级之上匹配最近的一个Provider来读取该context值。
参数defaultValue
只在组件没有在上层组件树中找到匹配的Provider
才会生效。这有助于在不包装组件的情况下测试它们。注意:传递undefined
给Provider时消费组件的defaultValue不会生效。
Context.Provider
<MyContext.Provider value={/* 某个值 */}>
每一个context对象都返回一个React组件,它允许消费组件实时更新值的变化。
Provider接收一个value
作为prop并将其传递给它的子消费组件。一个Provider可以和多个消费组件有对应关系。Provider之间也可以相互嵌套并且深层次的value值会覆盖其他的值。
当Provider的value值更新时,它内部的所有消费组件都会重新渲染。Provider和它内部组件的value值传递不受限于shouldComponentUpdate
函数,因此当消费组件的祖先组件停止更新时它也可以更新。
根据新旧值来决定是否更新使用的是与Object.is相同的算法。
注意:
当传递value给对象时,检测数据变化的方法可能会导致一些问题,详情请查看注意事项。
Class.contextType
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;
class的contextType
属性会被重赋值为一个通过React.createContext()创建的context对象。这能让你通过使用this.context
来消费最近的context上的值。你可以在任何生命周期方法中使用它,包括render方法。
注意:
通过这个API你只能订阅一个context。如果你需要订阅多个context,请查看使用多个context。
如果你在使用实验性的public class fileds语法,你可以使用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 => /*根据context值渲染一些数据 */}
</MyContext.Consumer>
这里,React组件也可以获取到context的变更,这能让你在函数式组件中完成订阅。
需要函数作为子元素。这个函数接收当前context的值并返回一个React的节点。value入参的值等同于高层级上最近的Provider的value值。如果在更高层级上没有对应的Provider,那么value入参的值等同于构建context时传入的defaultValue
值。
注意:
需要了解关于函数作为子元素模式的更多内容,请查看render props。
Context.displayName
context对象接收一个displayName字符串属性。React DevTools根据这个字符串来决定context要显式的值。
比如,下面的组件会在DevTools上显示MyDisplayName :
const MyContext = React.createContext(/* 一些值 */);
MyContext.displayName = 'MyDisplayName';
<MyContext.Provider> // "MyDisplayName.Provider" in DevTools
<MyContext.Consumer> // "MyDisplayName.Consumer" in DevTools
示例
动态context
对于上面的theme示例,使用动态值后更复杂的用法:
theme-context.js
export const themes = {
light: {
foreground: '#000000',
background: '#eeeeee',
},
dark: {
foreground: '#ffffff',
background: '#222222',
},
};
export const ThemeContext = React.createContext(
themes.dark // default value
);
themed-button.js
import {ThemeContext} from './theme-context';
class ThemedButton extends React.Component {
render() {
let props = this.props;
let theme = this.context;
return (
<button
{...props}
style={{backgroundColor: theme.background}}
/>
);
}
}
ThemedButton.contextType = ThemeContext;
export default ThemedButton;
app.js
import {ThemeContext, themes} from './theme-context';
import ThemedButton from './themed-button';
// 一个使用ThemedButton组件的中间组件
function Toolbar(props) {
return (
<ThemedButton onClick={props.changeTheme}>
Change Theme
</ThemedButton>
);
}
class App extends React.Component {
constructor(props) {
super(props);
this.state = {
theme: themes.light,
};
this.toggleTheme = () => {
this.setState(state => ({
theme:
state.theme === themes.dark
? themes.light
: themes.dark,
}));
};
}
render() {
//在ThemeProvider内部的ThemedButton按钮
//使用了state中存储的theme,而在Provider外部
//的按钮使用了默认的dark theme
return (
<Page>
<ThemeContext.Provider value={this.state.theme}>
<Toolbar changeTheme={this.toggleTheme} />
</ThemeContext.Provider>
<Section>
<ThemedButton />
</Section>
</Page>
);
}
}
ReactDOM.render(<App />, document.root);
在嵌套组件中更新context
在位于组件树深层的嵌套组件中更新context是非常重要的。在这种情况下你可以通过context传递一个函数来让消费组件更新context:
theme-context.js
//确保传递给createContext的数据结构与消费组件
//所需要的相匹配
export const ThemeContext = React.createContext({
theme: themes.dark,
toggleTheme: () => {},
});
theme-toggler-button.js
import {ThemeContext} from './theme-context';
function ThemeTogglerButton() {
//ThemeToggleButton组件不仅从context接收了theme
//还接收了toggleTheme函数
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包含了更新函数
//因此它也会被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);
Consuming Multiple Contexts
使用多个context
为了保证context能快速地重新渲染,React需要每一个consumer组件的context成为组件树上单独的节点。
// Theme context,默认值为light
const ThemeContext = React.createContext('light');
// 登录用户context
const UserContext = React.createContext({
name: 'Guest',
});
class App extends React.Component {
render() {
const {signedInUser, theme} = this.props;
//App组件提供了context的初始值
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>
);
}
如果两个以上的context值经常被一起使用,你就可以考虑构建能够同时提供这些值的渲染组件。
注意事项
因为context使用参考标识(reference identity)来决定什么时候渲染,这里可能会有一些陷阱,当provider的父组件重新渲染时,可能在consumer组件中触发一些无意识的渲染。比如下面的代码中每一次Provider重新渲染时由于value
属性都会被赋予一个新的对象,在它之下的所有consumer组件都会重新渲染:
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>
);
}
}
过时的API
注意:
先前React使用实验性的context API运行。老版的API在所有的16.x版本中都会得到支持,但用到它的应用应该迁移到新的版本。过时的API将会在未来的版本中被移除。阅读过时的context文档了解更多。