Context提供了一种通过组件树传递数据的方法,而不必在每个级别手动传递props。
在典型的React应用程序中,数据通过属性自顶向下(父级到子级)传递,但对于应用程序中的许多组件都需要的某些类型的属性(例如locale preference、UI主题)来说,这可能很麻烦。Context提供了一种在组件之间共享这些值的方法,而不必显式地在树的每一层传递属性。
什么时候使用Context
上下文的设计目的是共享可以被认为是React组件树的“全局”数据,比如当前经过身份验证的用户、主题或首选语言。例如,在下面的代码中,我们手动传递一个“theme”属性,以便给按钮组件添加样式:
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 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} />;
}
}
使用上下文之前
上下文主要用于在不同嵌套级别的许多组件需要访问某些数据时。谨慎地应用它,因为它使组件重用更加困难。
如果只想避免通过多个级别传递一些道具,组件组合通常是比上下文更简单的解决方案。
例如,一个Page
组件跨级别传递user
和avatarSize
属性,以便内嵌的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
属性,那么将这两个属性传递给多个级别可能会让人觉得多余。同样令人恼火的是,每当Avatar
组件需要更多来自顶层的属性时,你也必须在所有的中间层添加它们。
除了上下文之外的一个解决方式是,将Avatar
组件本身向下传递,这样中间组件就不需要关心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} />;
}
// Now, we have:
<Page user={user} avatarSize={avatarSize} />
// ... which renders ...
<PageLayout userLink={...} />
// ... which renders ...
<NavigationBar userLink={...} />
// ... which renders ...
{props.userLink}
通过这种改动,只有最顶层的Page
组件需要了解Link
和Avatar
组件关于user
及avatarSize
属性的使用。
这种控制反转在很多情况下可以使代码更简洁,因为它减少了需要通过应用程序传递的属性数量,并为根组件提供了更多的控制。然而,在很多情况下,这都不是正确的选择:将复杂度在树中上移让高级别的组件看起来更复杂难懂,同时也迫使较低级别的组件比您可能希望的要更灵活。
您并不局限于一个组件的单个子组件。您可以传递多个子节点,甚至可以为子节点提供多个单独的“插槽”,如下所示:
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允许您将这些数据“广播”给下面的所有组件,并对其进行更改。使用上下文可能比其他方法更简单的常见示例包括管理当前地区、主题或数据缓存。
API
React.createContext
const MyContext = React.createContext(defaultValue);
创建上下文对象。当React渲染的组件订阅此上下文对象时,它将从树中与其最匹配的Provider
处读取当前上下文值。
defaultValue
参数仅当组件在树中没有匹配的提供程序时才使用。这有助于在不封装组件的情况下独立测试组件。注意:传递undefined
作为提生产者的值并不会导致使用组件使用defaultValue
。
Context.Provider
<MyContext.Provider value={/* some value */}>
每个上下文对象都带有一个Provider React组件,该组件允许消费者组件订阅上下文更改。
Provider
接收一个value
属性,并将他传给后继的消费者组件。一个生产者可以和多个消费者建立连接。生产者可以被嵌套,用以覆盖树中更深的值。
无论何时,当生产者的value
值改变时,它的后继消费者组件都会被重新渲染。生产者到消费者之间的传递并不受shouldComponentUpdate
方法支配,因此,即使祖先组件退出更新,使用者也会被更新。
更改是通过使用与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
属性分配一个由response.createcontext()
创建的上下文对象。这允许您使用this.context
使用该上下文类型最近的当前值。您可以在任何生命周期方法(包括render函数)中引用它。
注意:您只能使用此API订阅单个上下文。如果需要读取多个上下文,请参阅
Consuming Multiple Contexts
。如果你正在使用实验性的public class fields syntax
,可以使用静态类字段初始化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>
订阅上下文更改的React组件。这允许您订阅函数组件中的上下文。
需要一个函数作为子节点。函数接收当前上下文值并返回一个React节点。传递给函数的value
参数将等于树中此上下文的最近提供者的value
属性。如果上面没有此上下文的生产者,value
参数将等于传递给createContext()
的defaultValue
。
注意:有关“函数为子节点”的更多信息,请参见render props。
Context.displayName
上下文对象接收一个displayName
属性。React DevTools使用这个字符串来确定要为上下文显示什么。
例如,以下组件将作为MyDisplayName出现在DevTools中:
const MyContext = React.createContext(/* some value */);
MyContext.displayName = 'MyDisplayName';
<MyContext.Provider> // "MyDisplayName.Provider" in DevTools
<MyContext.Consumer> // "MyDisplayName.Consumer" in DevTools
Examples
Dynamic 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);
内嵌组件中更新上下文
通常需要从嵌套在组件树中某个位置的组件更新上下文。在这种情况下,您可以通过上下文向下传递一个函数,以允许消费者更新上下文:
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);
使用多个上下文
为了保持上下文的快速重新呈现,React需要使每个上下文使用者成为树中的一个单独节点。
// 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>
);
}
如果经常同时使用两个或多个上下文值,您可能需要考虑创建自己的渲染属性组件,该组件同时提供这两个值。
警告
因为上下文使用引用标识来确定何时重新渲染,所以当提供者的父类重新渲染时,可能会触发使用者中的一些意外渲染。例如,下面的代码将在每次提供者重新渲染时重新渲染所有消费者,因为总是为value
创建一个新对象:
class App extends React.Component {
render() {
return (
<Provider value={{something: 'something'}}>
<Toolbar />
</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>
);
}
}