image.png
通过本篇文章,我们要弄清楚

  • Why React Context?
  • What is React Context?
  • How to use React Context?
  • When to use React Context?
  • Change Context into useContext?

let’s begin!

Why?

在我们的应用中,通常会出现将props层层传递props,如下图,中间的组件可能不需要props,但是为了使得C获取必须传递props
image.png

Context 提供了一个无需为每层组件手动添加 props,就能在组件树间进行数据传递的方法。

image.png

Context 能让你将这些数据向组件树下所有的组件进行“广播”,所有的组件都能访问到这些数据,也能访问到后续的数据更新。
顶部组件通过Provider来提供context,包裹的任何子组件可以通过Consume来获取context

What are use cases for React Context?
假设我们的应用中有个主题颜色需要设置,应用中有A、B、C、D、E组件,包裹的组件需要知道主题颜色。
因此顶级组件需要使得子组件获取到主题
image.png
Who provides/consumes React Context?
A组件是顶级组件,提供Context
C组件是其中一个子组件,消费使用Context
本例子中B、D、E组件不需要主题Context,当然如果需要也可以消费的

When?

通常有两种情况需要使用Context

  • 组件嵌套很深 且 需要层层传递props
  • When you want to have advanced state management in React with React Hooks for passing state and state updater functions via React Context through your React application. Doing it via React Context allows you to create a shared and global state.

    Context Api概览

    React.createContext

    1. const MyContext = React.createContext(defaultValue);

创建一个 Context 对象。当 React 渲染一个订阅了这个 Context 对象的组件,这个组件会从组件树中离自身最近的那个匹配的 Provider 中读取到当前的 context 值。
只有当组件所处的树中没有匹配到 Provider 时,其 defaultValue 参数才会生效。这有助于在不使用 Provider 包装组件的情况下对组件进行测试。注意:将 undefined 传递给 Provider 的 value 时,消费组件的 defaultValue 不会生效。

Context.Provider

  1. <MyContext.Provider value={/* 某个值 */}>

每个 Context 对象都会返回一个 Provider React 组件,它允许消费组件订阅 context 的变化。
Provider 接收一个 value 属性,传递给消费组件。一个 Provider 可以和多个消费组件有对应关系。多个 Provider 也可以嵌套使用,里层的会覆盖外层的数据。
当 Provider 的 value 值发生变化时,它内部的所有消费组件都会重新渲染。Provider 及其内部 consumer 组件都不受制于 shouldComponentUpdate 函数,因此当 consumer 组件在其祖先组件退出更新的情况下也能更新。

Class.contextType

  1. class MyClass extends React.Component {
  2. componentDidMount() {
  3. let value = this.context;
  4. /* 在组件挂载完成后,使用 MyContext 组件的值来执行一些有副作用的操作 */
  5. }
  6. componentDidUpdate() {
  7. let value = this.context;
  8. /* ... */
  9. }
  10. componentWillUnmount() {
  11. let value = this.context;
  12. /* ... */
  13. }
  14. render() {
  15. let value = this.context;
  16. /* 基于 MyContext 组件的值进行渲染 */
  17. }
  18. }
  19. MyClass.contextType = MyContext;

挂载在 class 上的 contextType 属性会被重赋值为一个由 React.createContext() 创建的 Context 对象。这能让你使用 this.context 来消费最近 Context 上的那个值。你可以在任何生命周期中访问到它,包括 render 函数中。

  1. class MyClass extends React.Component {
  2. static contextType = MyContext;
  3. render() {
  4. let value = this.context;
  5. /* 基于这个值进行渲染工作 */
  6. }
  7. }

Context.Consumer

  1. <MyContext.Consumer>
  2. {value => /* 基于 context 值进行渲染*/}
  3. </MyContext.Consumer>

一个 React 组件可以订阅 context 的变更,这让你在函数式组件中可以订阅 context。
这种方法需要一个函数作为子元素(function as a child)。这个函数接收当前的 context 值,并返回一个 React 节点。传递给函数的 value 值等价于组件树上方离这个 context 最近的 Provider 提供的 value 值。如果没有对应的 Provider,value 参数等同于传递给 createContext()defaultValue

Context.displayName

context 对象接受一个名为 displayName 的 property,类型为字符串。React DevTools 使用该字符串来确定 context 要显示的内容。
示例,下述组件在 DevTools 中将显示为 MyDisplayName:

  1. const MyContext = React.createContext(/* some value */);
  2. MyContext.displayName = 'MyDisplayName';
  3. <MyContext.Provider> // "MyDisplayName.Provider" 在 DevTools 中
  4. <MyContext.Consumer> // "MyDisplayName.Consumer" 在 DevTools 中

How?

首先,通过createContext创建 React Contex,使得我们可以生成Provider组件和Consumer组件。
createContext的初始值可以是null。

  1. // src/ThemeContext.js
  2. import React from 'react';
  3. const ThemeContext = React.createContext(null);
  4. export default ThemeContext;

组件A是顶级组件,生成Provider,提供了value值,可以使得嵌套在A组件的子组件可以获取value

  1. import React from "react";
  2. import D from "./ComponentD.js";
  3. import ThemeContext from "./ThemeContext";
  4. const A = () => (
  5. <ThemeContext.Provider value="green">
  6. <D />
  7. </ThemeContext.Provider>
  8. );
  9. export default A;

组件D嵌套了组件C,不需要组件D传递value,C可以直接消费获取到value

  1. import React from "react";
  2. import C from "./ComponentC.js";
  3. import ThemeContext from "./ThemeContext";
  4. const D = () => (
  5. <h3>
  6. 我是D组件 内部嵌套C <C />
  7. </h3>
  8. );
  9. export default D;

组件C中获取通过Consumer来获取value

  1. // src/ComponentC.js
  2. import React from "react";
  3. import ThemeContext from "./ThemeContext";
  4. const C = () => (
  5. <ThemeContext.Consumer>
  6. {value => <p style={{ color: value }}>C组件消费了Context value:{value}</p>}
  7. </ThemeContext.Consumer>
  8. );
  9. export default C;

以上就是一个简易的Context使用场景
代码见 https://stackblitz.com/edit/react-vejzhc?file=src%2FApp.js

useContext

上面的例子中我们通过Consumer来获取,如果多个子组件都想获取使用value,我们不得不写很多这样的样板代码

  1. // src/ComponentC.js
  2. import React from "react";
  3. import ThemeContext from "./ThemeContext";
  4. // const C = () => (
  5. // <ThemeContext.Consumer>
  6. // {value => <p style={{ color: value }}>C组件消费了Context value:{value}</p>}
  7. // </ThemeContext.Consumer>
  8. // );
  9. const C = () => {
  10. const color = React.useContext(ThemeContext);
  11. return <p style={{ color }}>Hello World</p>;
  12. };
  13. export default C;

现在通过useContext同样可以获取到value,简洁。

提示 如果你在接触 Hook 前已经对 context API 比较熟悉,那应该可以理解,useContext(MyContext) 相当于 class 组件中的 static contextType = MyContext 或者 <MyContext.Consumer>useContext(MyContext) 只是让你能够读取 context 的值以及订阅 context 的变化。你仍然需要在上层组件树中使用 <MyContext.Provider> 来为下层组件提供 context。


Practice in Programe

项目中用户信息作为全局信息,有些页面组件需要用到用户信息

  1. import React, { useState, useEffect } from 'react';
  2. import M from '@block/utils';
  3. const Context = React.createContext();
  4. function UserProvider({ children }) {
  5. const [user, setUser] = useState(0);
  6. useEffect(() => {
  7. // 获取购物车数量
  8. async function fetchCartCount() {
  9. const res = await M.$fetch('/api/pr/c/prOrder/current');
  10. setUser(res);
  11. }
  12. fetchCartCount();
  13. }, []);
  14. return <Context.Provider value={user}>{children}</Context.Provider>;
  15. }
  16. export default Context;
  17. export { UserProvider };

在APP.js中

  1. import React, { useEffect } from 'react';
  2. import M from '@block/utils';
  3. import { Router, browserHistory, Route, withRouter, IndexRoute, Redirect } from 'react-router';
  4. import { Header, Footer, Helper } from '@/src/components';
  5. import routes from './routes';
  6. import Home from './nodes/home';
  7. // 单例
  8. import { UserProvider } from '@/src/components/UserContext';
  9. import { MonitorProvider } from '@/src/components/MonitorContext';
  10. import { CartProvider } from '@/src/components/CartContext';
  11. import { QuestionProvider } from '@/src/components/Question';
  12. let isGrayUser = null;
  13. const GrayUser = withRouter(({ children, location }) => {
  14. useEffect(() => {
  15. async function fetchGrayUser() {
  16. if (isGrayUser === null) {
  17. const data = await M.$fetch('/api/pr/c/auth/whiteList');
  18. isGrayUser = !data;
  19. }
  20. }
  21. fetchGrayUser();
  22. }, [location]);
  23. return children;
  24. });
  25. function App({ children, router, location, route }) {
  26. // useEffect(() => {
  27. // window.app.shenpiConfig = {
  28. // onBack: () => {
  29. // console.log('router', this);
  30. // // 回到待审批
  31. // // this.props.router.push('/toApprove');
  32. // },
  33. // onOperate: () => {
  34. // // 回到待审批
  35. // // window.updateApproveBillCount();
  36. // // this.props.router.push('/toApprove');
  37. // },
  38. // };
  39. // }, []);
  40. const isIframe = window.self !== window.parent;
  41. const pathName = window.location.pathname;
  42. return (
  43. <GrayUser>
  44. <UserProvider>
  45. <MonitorProvider>
  46. <CartProvider>
  47. <QuestionProvider>
  48. <div className="app-layout">
  49. {!isIframe && pathName !== '/home-page' && <Header />}
  50. <div className="app-main">{children}</div>
  51. {!isIframe && <Helper />}
  52. {!isIframe && pathName !== '/home-page' && <Footer />}
  53. </div>
  54. </QuestionProvider>
  55. </CartProvider>
  56. </MonitorProvider>
  57. </UserProvider>
  58. </GrayUser>
  59. );
  60. }
  61. export default function AppWrapper() {
  62. // 初始化用户收货地址
  63. useEffect(() => {
  64. M.$fetch('/api/manage/address/initialAddress');
  65. }, []);
  66. return (
  67. <Router history={browserHistory}>
  68. <Route path="/" component={App}>
  69. <IndexRoute component={Home} />
  70. <Redirect from="/approval" to="/approval/todo" />
  71. {routes.map(route => (
  72. <Route key={route.path} path={route.path} component={route.component}>
  73. {route.children &&
  74. route.children.map(c => (
  75. <Route key={c.path} path={c.path} component={c.component}>
  76. {/* {c.children &&
  77. c.children.map(v => <Route key={v.path} path={v.path} component={v.component}></Route>)} */}
  78. </Route>
  79. ))}
  80. </Route>
  81. ))}
  82. <Redirect from="/order-center/request" to="/order-center/request/all" />
  83. <Redirect from="/order-center/order" to="/order-center/order/all" />
  84. <Redirect from="/order-center/check" to="/order-center/check/all" />
  85. <Redirect from="/order-center/po-place-order-list" to="/order-center/po-place-order-list/all" />
  86. {/* 兼容旧链接 */}
  87. <Redirect from="/check/detail" to="/check/detail" />
  88. <Redirect from="/check/non-check-detail" to="/check/non-check-detail" />
  89. </Route>
  90. </Router>
  91. );
  92. }

页面中使用

  1. import UserContext from '../UserContext';
  2. const user = useContext(UserContext);

References:
https://www.robinwieruch.de/react-context
https://www.robinwieruch.de/react-usecontext-hook