上下文(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) {
// 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} />
</div>
);
}
class ThemedButton extends React.Component {
render() {
return <Button theme={this.props.theme} />;
}
}
使用 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 />
</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 。 请谨慎使用它,因为它使组件重用更加困难。
如果您只想避免在多个级别上传递一些 props ,那么 组件组合 component composition 通常比 Context 更简单。
例如,考虑一下,一个 Page
组件的 user
和 avatarSize
prop(属性) 要经过多少个层级传递 ,才能使深层嵌套的Link
和 Avatar
组件可以读取它们:
<Page user={user} avatarSize={avatarSize} />
// ... which renders ...
<PageLayout user={user} avatarSize={avatarSize} />
// ... which renders ...
<NavigationBar user={user} avatarSize={avatarSize} />
// ... which renders ...
<Link href={user.permalink}>
<Avatar user={user} size={avatarSize} />
</Link>
如果最终只有 Avatar 组件确实需要它,那么将 user
和 avatarSize
props(属性) 传递到多个层级可能会感到多余。同样令人讨厌的是,当 Avatar
组件从顶部需要更多的 props(属性) 时,你也必须在所有的中间层级的组件中添加它们。
在 没有 context 的情况下解决此问题的一种方法是 传递 Avatar
组件本身,这样中间层级的组件不需要知道 user
prop(属性) :
function Page(props) {
const user = props.user;
const userLink = (
<Link href={user.permalink}>
<Avatar user={user} size={props.avatarSize} />
</Link>
);
return <PageLayout userLink={userLink} />;
}
// Now, we have:
<Page user={user} />
// ... which renders ...
<PageLayout userLink={...} />
// ... which renders ...
<NavigationBar userLink={...} />
// ... which renders ...
{props.userLink}
通过这些更改,只有最顶层的 Page
组件知道 Link
和 Avatar
组件需要使用 user
和 avatarSize
props(属性)。
在许多情况下,通过减少应用程序需要的 props(属性) 数量并对根组件进行更多控制,这种 反向控制 可以使代码更加清晰。 但是,在每种情况下,这都不是正确的选择:在树中移动更高的复杂性会使这些更高级别的组件更加复杂,并迫使较低级别的组件比您想要的更灵活。
对于一个组件,您不限于单个的子组件。 你可以传递多个子组件,甚至可以为子组件们分别设置多个“插槽”,如下所述 :
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}
/>
);
}
当您需要将子组件与其直接父组件分离时,这种模式足以满足许多情况。 如果子组件需要在渲染前与父组件进程通信,那么您可以进一步使用渲染属性(render props)模式。
但是,有时需要树中的许多组件以及不同的嵌套级别可以访问相同的数据。 Context 允许您将此类数据“广播”到下面的所有组件并对其进行更改。 使用 Context 可能比替代方案更简单的常见示例包括管理当前本地设置,主题或数据缓存。
API
React.createContext
const MyContext = React.createContext(defaultValue);
创建一个 Context 对象对。当 React 渲染订阅这 个Context 对象的组件时,它将从组件树中匹配最接近的 Provider
中读取当前的 context 值。
defaultValue
参数 仅 当 consumer(使用者) 在树中没有匹配的 Provider(提供则) 时使用它。这有助于在不封装它们的情况下对组件进行测试。注意:将 undefined
作为 Provider(提供者) 值传递不会导致 consumer(使用者) 组件使用 defaultValue
。
Context.Provider
<MyContext.Provider value={/* some value */}>
每个 Context 对象都附带一个 Provider React组件,允许 consumer(使用者) 组件 来 订阅 context 的改变。
接受一个 value
属性传递给使用组件,这个 consumer(使用者) 组件 为 Provider(提供者) 的后代组件 。一个 Provider 可以连接到许多 consumers 。Provider(提供者) 可以被嵌套以覆盖树中更深层次的值。
每当 Provider(提供者) 的 value
属性发生变化时,所有作为 Provider(提供者) 后代的 consumer(使用者) 组件 都将重新渲染。 从Provider 到其后代使用者的传播不受 shouldComponentUpdate
方法的约束,因此即使祖先组件退出更新,也会更新 consumer(使用者) 。
通过使用与Object.is
相同的算法,比较新旧值来确定更改。
注意
确定更改的方式 在将对象作为
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;
可以为类上的 contextType
属性分配由 React.createContext()
创建的 Context 对象。 这允许您使用this.context
使用该 Context 类型 的最近的当前值。 您可以在任何生命周期方法中引用它,包括 render 函数。
注意:
您只能使用这个 API 订阅单个上下文。 如果您需要订阅多个,请参阅 使用多个 Contexts。
如果您使用的是实验性公共类字段语法,则可以使用 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>
一个可以订阅 context 变化的 React 组件。 这允许您订阅 函数式组件 中的 context 。
需要接收一个 函数作为子节点。 该函数接收当前 context 值并返回一个 React 节点。 传递给函数的 value
参数将等于组件树中上层这个 context 最接近的 Provider 的 value
属性。 如果上层没有提供这个 context 的 Provider ,value
参数将等于传递给 createContext()
的 defaultValue
。
注意
关于函数作为子节点的更多信息, 请看 render props。
示例
动态 Context
我们来看一下一个更加复杂的例子,主题的动态值:
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';
// An intermediate component that uses the 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() {
// The ThemedButton button inside the ThemeProvider
// uses the theme from state while the one outside uses
// the default 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 中向下传递一个函数,以允许 Consumer 更新 context :
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);
使用多个 context
为了保持 context 的快速重新渲染,React 需要使每个 context Consumer 成为树中的一个独立节点。
// 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 值,您可能需要考虑创建自己的渲染属性组件,同时提供两者。
告诫
因为 context 使用引用标识来确定何时重新渲染,当 Provider(提供者) 的父节点重新渲染时,有一些陷阱可能触发 Consumer(使用者) 无意渲染。例如,下面的代码将在每次 Provider(提供者) 重新渲染时,会重新渲染所有 Consumer(使用者) ,因为总是为 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>
);
}
}
遗留 API
注意
React 先前提供了一个实验性 context API 。所有 16.x 版本都将支持旧的 API,但使用它的应用程序应该迁移到新版本。遗留 API 将在未来的主要 React 版本中删除。阅读遗留 context 文档。