React Context
Context 通过组件树提供了一个传递数据的方法,从而避免了在每一个层级手动的传递 props 属性。在一个典型的 React 应用中,数据是通过 props 属性由上向下(由父及子)的进行传递的,但这对于某些类型的属性而 言是极其繁琐的(例如:地区偏好,UI 主题),这是应用程序中许多组件都所需要的。 Context 提供了一种在组件之间共享此类值的方式,而不必通过组件树的每个层级显式地传递 props 。
何时使用 Context
Context 设计目的是为共享那些被认为对于一个组件树而言是“全局”的数据,例如当前认证的用户、主题或首选语言。例如,在下面的代码中,我们通过一个 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>
);
}
}
不要仅仅为了避免在几个层级下的组件传递 props 而使用 context,它是被用于在多个层级的多个组件需要访问相同数据的情景,即 组件复用 的场景。
API
React.createContext
const MyContext = React.createContext(defaultValue);
创建一对 { Provider, Consumer }
。当 React 渲染 context 组件 Consumer 时,它将从组件树的上层中最接近的匹配的 Provider 读取当前的 context 值。
如果上层的组件树没有一个匹配的 Provider,而此时你需要渲染一个 Consumer 组件,那么你可以用到 defaultValue
。这有助于在不封装它们的情况下对组件进行测试。
Context.Provider
<MyContext.Provider value={/* some value */}>
React 组件允许 Consumers 订阅 context 的改变。
接收一个 value
属性传递给 Provider 的后代 Consumers。一个 Provider 可以联系到多个 Consumers。Providers 可以被嵌套以覆盖组件树内更深层次的值。
Context.Consumer
<MyContext.Consumer>
{value => /* render something based on the context value */}
</MyContext.Consumer>
一个可以订阅 context 变化的 React 组件。
接收一个函数作为子节点(即 Render Props)。 函数接收当前 context 的值并返回一个 React 节点。传递给函数的 value 将等于组件树中上层 context 的最近的 Provider 的 value
属性。如果 context 没有 Provider ,那么 value 参数将等于被传递给 createContext()
的 defaultValue
。
每当 Provider 的值发生改变时, 作为 Provider 后代的所有 Consumers 都会重新渲染。 从 Provider 到其后代的 Consumers 传播不受 shouldComponentUpdate
方法的约束,因此即使祖先组件没有更新,后代 Consumer 也会被更新。
因为内部通过使用与 Object.is
相同的算法比较新值和旧值来确定变化。所以为了避免一些可能触发意外的渲染,可以将提升 value
到父节点的 state 里。
因为 context 使用 reference identity
确定何时重新渲染,在 Consumer 中,当一个 Provider 的父节点重新渲染的时候,有一些问题可能触发意外的渲染。例如下面的代码,所有的 Consumer 在 Provider 重新渲染之时,每次都将重新渲染,因为一个新的对象总是被创建对应 Provider 里的 value:
class App extends React.Component {
render() {
return (
<Provider value={{something: 'something'}}>
<Toolbar />
</Provider>
);
}
}
为了防止这样, 提升 value
到父节点的 state 里:
class App extends React.Component {
constructor(props) {
this.state = {
value: {something: 'something'}
};
}
render() {
return (
<Provider value={this.state.value}>
<Toolbar />
</Provider>
);
}
}
Context.displayName
Context 对象接受一个 displayName
字符串属性。 React DevTools 用这个字段去来确定要为 Context 显示的内容。.
例如, 以下组件将在 DevTools 中显示为 MyContext:
const MyContext = React.createContext(/* some value */);
MyContext.displayName = 'MyDisplayName';
<MyContext.Provider> // "MyDisplayName.Provider" in DevTools
<MyContext.Consumer> // "MyDisplayName.Consumer" in DevTools
// TODO: add screenshots
Class.contextType
可以将由 React.createContext()
创建的 context 对象赋值给 class 的 contextType
属性。 然后使用 this.context
即可拿到该值,你可以在任何生命周期方法(包括 render 函数)中使用 this.context
。如下例所示:
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;
使用这个 API,你只能订阅单个 context。 如果你想订阅多个 context,请参阅 使用多个 Contexts。
如果你在使用实验性的 public class fields
语法, 你可以使用 static class field
来初始化你的 contextType
:
class MyClass extends React.Component {
static contextType = MyContext;
render() {
let value = this.context;
/* render something based on the value */
}
}
useContext
useContext
是 react 中三大基础 hooks 之一,使用方式如下:
const value = useContext(MyContext);
useContext
接收一个 context 对象然后返回该 context 的值。当 <MyContext.Provider>
更新时,此 Hook 将触发重新渲染。
不要忘记传入的参数是 Context。
- Correct: useContext(MyContext)
- Incorrect: useContext(MyContext.Consumer)
- Incorrect: useContext(MyContext.Provider)
当 context 值更改时,调用 useContext 的组件将始终重新渲染。如果重新渲染组件代价比较大,则可以使用 useMemo
对其进行优化,其实在 react-redux 内部也是如此。
function Button() {
let appContextValue = useContext(AppContext);
let theme = appContextValue.theme; // Your "selector"
return useMemo(() => {
// The rest of your rendering logic
return <ExpensiveTree className={theme} />;
}, [theme]);
}
另外,useContext(MyContext)
仅仅用来读取 context 的值和订阅其更新,但仍然需要使用 <MyContext.Provider>
。
useContext(MyContext)
等价于 class 中的 static contextType = MyContext
或者 MyContext.Consumer
。
Examples
使用多个 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 值一起使用,你可以考虑使用 render props 的方法。
更加复杂的例子请参考:https://reactjs.org/docs/context.html#examples
Context 在 react-redux 中的应用
其实原理机制很普通,也是应用 React.createContext
方法:
ReactReduxContext = React.createContext(null);
封装了一下 Provider:
function Provider({store, context, children}) {
// ...
const Context = context || ReactReduxContext;
return <Context.Provider value={contextValue}>{children}</Context.Provider>;
}
在 connect
HOC 里面本质上的代码与以下实现类似:
function connect (mapStateToProps, mapDispatchToProps) {
return function wrap(WrappedComponent) {
return function ConnectComponent(props) {
return (
<Context.Consumer>
{store => (
<WrappedComponent
{...props}
{...mapStateToProps(store.getState(), props)}
{...mapDispatchToProps(store.dispatch, props)}
/>)
}
<Context.Consumer>
)
}
}
}
详细实现请参考源码:https://github.com/reduxjs/react-redux