1.何时使用context?
使用场景:共享那些被认为对于一个组件树而言是“全局”的数据,例如当前认证的用户、主题或首选语言。
如下面:Toobar->ThemedButton->Button,传递属性:theme
function ThemedButton(props) {
return <Button theme={props.theme} />;
}
// 中间组件
function Toolbar(props) {
// Toolbar 组件必须添加一个额外的 theme 属性
// 然后传递它给 ThemedButton 组件
return (
<div>
<ThemedButton theme={props.theme} />
</div>
);
}
class App extends React.Component {
render() {
return <Toolbar theme="dark" />;
}
}
优化:使用context,避免中间元素传递props:
// 创建一个 theme Context, 默认 theme 的值为 light
const ThemeContext = React.createContext('light');
function ThemedButton(props) {
// ThemedButton 组件从 context 接收 theme
return (
<ThemeContext.Consumer>
{theme => <Button {...props} theme={theme} />}
</ThemeContext.Consumer>
);
}
// 中间组件
function Toolbar(props) {
return (
<div>
<ThemedButton />
</div>
);
}
class App extends React.Component {
render() {
return (
<ThemeContext.Provider value="dark">
<Toolbar />
</ThemeContext.Provider>
);
}
}
可以在控制台验证。
结论:
(1)当 React 渲染 context 组件 Consumer时,优先从最接近的provider中读取当前的context值。如果上层的组件树没有一个匹配的 Provider,那么你可以从 defaultValue读取其默认值。
(2)不要仅仅为了避免在几个层级下的组件传递 props 而使用 context,它是被用于在多个层级的多个组件需要访问相同数据的情景。
API
React.createContext
const {Provider, Consumer} = React.createContext(defaultValue);
创建一对 { Provider, Consumer }。
Provider
<Provider value={/* some value */}>
Provider:接受一个value属性,并将它传递给自己的后代Consumers。
Providers 可以被嵌套以覆盖组件树内更深层次的值。
Consumer
<Consumer>
{value => /* render something based on the context value */}
</Consumer>
用来订阅 context 变化的 React 组件。
接收一个 函数作为子节点. 函数接收当前 context 的值并返回一个 React 节点。
订阅到的value值:优先找组件树中上层 context 的最近的 Provider 的 value 属性,如果没有找到,将从createContext() 的 defaultValue读取value值。
应用
1.动态订阅主题
思路:ThemeContext(通过state绑定)->Toolbar->ThemedButton->button
theme-context.js(声明全局context)
export const themes = {
light: {
foreground: '#ffffff',
background: '#222222',
},
dark: {
foreground: '#000000',
background: '#eeeeee',
},
};
export const ThemeContext = React.createContext(
themes.dark // 默认值
);
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 './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() {
// ThemedButton 位于 ThemeProvider 内
// 在外部使用时使用来自 state 里面的 theme
// 默认 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);
2.父子耦合
上面例子是:将顶层provider绑定状态值,通过状态值改变,来动态改变context值。
下面讨论场景:从组件树中某个深度嵌套的组件中更新 context,设计思路:通过 context 向下传递一个函数,以允许 Consumer 更新 context 。
如下:ThemeContext(传递可以更新context的函数)->content->ThemeTogglerButton->button(绑定可以更新context的函数)
theme-context.js
// 确保默认值按类型传递
// createContext() 匹配的属性是 Consumers 所期望的
export const ThemeContext = React.createContext({
theme: themes.dark,
toggleTheme: () => {},
});
theme-toggler-button.js
import {ThemeContext} from './theme-context';
function ThemeTogglerButton() {
// Theme Toggler 按钮不仅接收 theme 属性
// 也接收了一个来自 context 的 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 包含了 updater 函数 所以它可以传递给底层的 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);
3.作用于多个上下文
当作用于多个上下文时,React 需要使每一个 Consumer 在组件树中成为一个单独的节点。
建议:如果两个或者多个上下文的值经常被一起使用,推荐考虑你自己渲染属性的组件。
// 主题上下文, 默认light
const ThemeContext = React.createContext('light');
// 登陆用户上下文
const UserContext = React.createContext();
// 一个依赖于两个上下文的中间组件
function Toolbar(props) {
return (
<ThemeContext.Consumer>
{theme => (
<UserContext.Consumer>
{user => (
<ProfilePage user={user} theme={theme} />
)}
</UserContext.Consumer>
)}
</ThemeContext.Consumer>
);
}
class App extends React.Component {
render() {
const {signedInUser, theme} = this.props;
// App组件提供上下文的初始值
return (
<ThemeContext.Provider value={theme}>
<UserContext.Provider value={signedInUser}>
<Toolbar />
</UserContext.Provider>
</ThemeContext.Provider>
);
}
}
4.在生命周期方法中访问 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>
);
5.高阶组件中的context
引入:下面代码,可以产生主题按钮。但是,如果要订阅主题输入框等其他主题组件呢?
const ThemeContext = React.createContext('light');
function ThemedButton(props) {
return (
<ThemeContext.Consumer>
{theme => <button className={theme} {...props} />}
</ThemeContext.Consumer>
);
}
使用高阶组件中的context,快速订阅主题组件。即传入普通组件,使用高阶函数withTheme进行封装。
高阶函数:
const ThemeContext = React.createContext('light');
// 在函数中引入组件
export function withTheme(Component) {
// 然后返回另一个组件
return function ThemedComponent(props) {
// 最后使用context theme渲染这个被封装组件
// 注意我们照常引用了被添加的属性
return (
<ThemeContext.Consumer>
{theme => <Component {...props} theme={theme} />}
</ThemeContext.Consumer>
);
};
}
使用:
function Button({theme, ...rest}) {
return <button className={theme} {...rest} />;
}
const ThemedButton = withTheme(Button);