上下文(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>
);
}
function ThemedButton(props) {
return <Button theme={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>
);
}
function ThemedButton(props) {
// Use a Consumer 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".
return (
<ThemeContext.Consumer>
{theme => <Button {...props} theme={theme} />}
</ThemeContext.Consumer>
);
}
注意
不要只是为了避免在几个层级下的组件传递 props 而使用 context。 坚持需要在多个层次的许多组件中访问相同数据的情况下使用 context 。
API
React.createContext
const {Provider, Consumer} = React.createContext(defaultValue);
创建一个 { Provider, Consumer }
对。当 React 渲染 context Consumer
时,它将从组件树中匹配最接近的 Provider
中读取当前的 context 值。
defaultValue
参数 仅 当 Consumer(使用者) 在树中没有匹配的 Provider(提供则) 时使用它。这有助于在不封装它们的情况下对组件进行测试。注意:将 undefined
作为 Provider(提供者) 值传递不会导致 Consumer(使用者) 使用 defaultValue
。
Provider
<Provider value={/* some value */}>
React组件允许 Consumer(使用者) 订阅 context 的改变。
接受一个 value
属性传递给 Provider(提供则) 的后代的 Consumer(使用者) 。
一个 Provider 可以连接到许多 Consumers 。
Providers 可以被嵌套以覆盖树中更深层次的值。
Consumer
<Consumer>
{value => /* render something based on the context value */}
</Consumer>
一个可以订阅 context 变化的 React 组件。
需要接收一个 函数作为子节点。 该函数接收当前 context 值并返回一个 React 节点。 传递给函数的 value
参数将等于组件树中上层这个 context 最接近的 Provider 的 value
属性。 如果上层没有提供这个 context 的 Provider ,value
参数将等于传递给 createContext()
的 defaultValue
。
注意
关于函数作为子节点的更多信息, 请看 render props.
只要 Provider 的 value
属性发生变化是,所有属于该 Provider 后代的 Consumers 就会重新渲染。
从 Provider 到它的后代 Consumers 的传播不受 shouldComponentUpdate
方法的约束,
所以即使当祖先组件退出更新时,后代 Consumer 也会被更新。
通过使用与Object.is相同的算法比较新值和旧值来确定 value
属性变化。
注意
当传递对象作为
value
时,在确定value
属性是否变化时引发一些问题:Caveats。
示例
动态 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';
function ThemedButton(props) {
return (
<ThemeContext.Consumer>
{theme => (
<button
{...props}
style={{backgroundColor: theme.background}}
/>
)}
</ThemeContext.Consumer>
);
}
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
在生命周期方法中访问 context 值是一种相对常见的用例。 不是将 context 添加到每个生命周期方法中, 你只需将它作为 props , 然后像使用 props 一样使用它即可。
class Button extends React.Component {
componentDidMount() {
// ThemeContext value is this.props.theme
}
componentDidUpdate(prevProps, prevState) {
// Previous ThemeContext value is prevProps.theme
// New ThemeContext value is this.props.theme
}
render() {
const {theme, children} = this.props;
return (
<button className={theme ? 'dark' : 'light'}>
{children}
</button>
);
}
}
export default props => (
<ThemeContext.Consumer>
{theme => <Button {...props} theme={theme} />}
</ThemeContext.Consumer>
);
高阶组件中的 Context
某些类型的 context 被许多组件(例如 theme 或 localization )使用。使用 <Context.Consumer>
元素显示地封装每个依赖项是冗余的。这里 高阶组件 可以帮助我们解决这个问题。
例如,按钮组件可能会使用一个 主题(theme) context,如下所示:
const ThemeContext = React.createContext('light');
function ThemedButton(props) {
return (
<ThemeContext.Consumer>
{theme => <button className={theme} {...props} />}
</ThemeContext.Consumer>
);
}
对于少量组件来说,这样做是可以的,但是如果我们想在很多地方使用 theme context 呢?
我们可以创建一个名为 withTheme
的高阶组件:
const ThemeContext = React.createContext('light');
// This function takes a component...
export function withTheme(Component) {
// ...and returns another component...
return function ThemedComponent(props) {
// ... and renders the wrapped component with the context theme!
// Notice that we pass through any additional props as well
return (
<ThemeContext.Consumer>
{theme => <Component {...props} theme={theme} />}
</ThemeContext.Consumer>
);
};
}
现在,任何依赖于 theme context 的组件都可以使用我们创建的 withTheme
函数轻松订阅它:
function Button({theme, ...rest}) {
return <button className={theme} {...rest} />;
}
const ThemedButton = withTheme(Button);
转发 Refs 给 context Consumer(使用者)
渲染 prop(属性) API的一个问题是 refs 不会自动传递给封装元素。 为了解决这个问题,使用 React.forwardRef
:
fancy-button.js
class FancyButton extends React.Component {
focus() {
// ...
}
// ...
}
// Use context to pass the current "theme" to FancyButton.
// Use forwardRef to pass refs to FancyButton as well.
export default React.forwardRef((props, ref) => (
<ThemeContext.Consumer>
{theme => (
<FancyButton {...props} theme={theme} ref={ref} />
)}
</ThemeContext.Consumer>
));
app.js
import FancyButton from './fancy-button';
const ref = React.createRef();
// Our ref will point to the FancyButton component,
// And not the ThemeContext.Consumer that wraps it.
// This means we can call FancyButton methods like ref.current.focus()
<FancyButton ref={ref} onClick={handleClick}>
Click me!
</FancyButton>;
告诫
因为 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 文档。