如何使用React Context API做全局状态管理

2,656 阅读6分钟

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

——React官方教程

在React世界,状态管理的实现方案百花齐放。对于简单的项目,可以使用状态提升或组合组件(通过传递包含状态的jsx对象)解决问题;稍微复杂的项目可以使用EventEmitter或umi推出的useModel,大型项目可以使用redux或mobx等专业的状态管理库。

今天要介绍一个React官方数据传递解决方案即:React Context。Redux这种大型状态管理库底层会用到React Context,很多中高级React开发人员也会使用React Context封装自己的小型状态管理库。诸如Ant Design Pro这类开箱即用的中台前端解决方案中都会封装自己的一套状态管理库,但是很多时候我们在开发一些项目的时候并不需要如此强大的功能,这时候我们就可以使用React Context API封装自己的状态管理库,所以今天笔者就介绍下如何使用Context API共享数据。

一、环境搭建

使用React官方推出的 create-react-app 脚手架很容易的就能搭建我们的实验环境,具体操作如下:

  1. 安装 create-react-app
yarn global add create-react-app
  1. 在任意目录下使用 create-react-app创建演示项目
create-react-app react-learning
  1. 创建组件(下面内容),并挂载到src/index.js下面的ReactDOM.render()函数中。

二、基于React Context共享数据

鉴于React Hook出现后,函数式组件成为主流,所以本文使用函数式组件演示,类组件使用方式类似,不做赘述。

1. 创建:Home.jsx组件,作为所有页面的入口:
import React, { useState } from "react";
import FirstChild from "./FirstChild";

// 创建一个共享 theme的 Context对象,并解构出Provider和Consumer对象
export const { Provider, Consumer } = React.createContext();
export default () => {
  //使用hook创建theme内容
  const [theme, setTheme] = useState({ name: "主题1", color: "red" });
  return (
    //  通过Provider 的value属性注入想要传递的数据
    <Provider value={theme}>
      <div>
        <p>父组件定义的值:{JSON.stringify(theme)}</p>
        <FirstChild />
        <button
          onClick={() => {
            // 改变theme对象内容
            setTheme((oldVal) => {
              return { ...oldVal, name: "主题2" };
            });
          }}
        >
          改变主题
        </button>
      </div>
    </Provider>
  );
};

在这个组件中,我们首先使用React.CreateContext()函数创建了一个Context对象,并解构出了Provider和Consumer两个对象。Provider主要是用来共享数据,而Consumer用来消费数据。在创建时,通过export命令将这两个对象暴露出去。 接着,使用useState hook创建了一个名为theme的对象,给定了包含name和color两个属性的Object对象作为初始值。在Provider组件上我们通过传递value属性将其作为Context数据进行绑定。我们在Home组件上使用JSON.Stringify()函数打印了theme对象的数据。最后我们创建了一个button按钮,点击按钮,会更新theme的数据。

2. 创建:FirstChild.jsx组件,尝试接收Context中的内容
import React from "react";
//引入Context对象的Consumer以消费数据
import { Consumer } from "./Home.jsx"; 
import GrandChild from "./GrandChild.jsx"; 
function FirstChild(props) {
  return (
    <Consumer>
      {/* 通过回调函数拿到共享的数据 */}
      {(theme) => (
        <>
          <p>子组件获取的数据:{JSON.stringify(theme)}</p>
          <GrandChild />
        </>
      )}
    </Consumer>
  );
}
export default FirstChild;

在第二个组件中,我们import了Home.jsx中export的Consumer对象,以消费Context中共享的数据。在不使用useContext hook的情况下,子组件必须使用<Consumer>包裹才能拿到共享的数据。我们通过回调函数拿到了共享的数据并使用JSON.stringify()函数打印。

3. 创建:GrandChild.jsx组件,尝试跨组件接收Context中的数据
import React from "react";
//引入Context对象的Consumer以消费数据
import { Consumer } from "./Home"; 
const GrandChild = (props) => {
  return (
    <Consumer>
      {(theme) => <p>孙子组件获取的数据:{JSON.stringify(theme)}</p>}
    </Consumer>
  );
};
export default GrandChild;

在这个组件中,我们首先声明了一个函数式组件,同样引入了Home组件中export的Consumer对象,并包裹了消费数据的函数,在这个孙子组件中我们同样拿到了Home组件中共享的theme数据。

before.png

三个组件都拿到了Context中共享的数据(修改前)

after.png

点击按钮修改对象后,同样触发了所有组件的重新渲染

通过上面三个组件的演示,可以发现,使用React Context可以很轻松的共享状态数据,不管是基本类型数据,还是引用类型的数据。现在可以对具体的使用步骤做个总结:

  1. 使用React.createContext()创建Context对象(可以指定默认值);
  2. 通过解构拿到Provider自定义组件以共享数据,拿到Consumer组件以消费数据;
  3. 使用Provider包裹所有需要消费数据的组件,并通过value属性传递需共享数据内容;
  4. 在子组件中引用Consumer组件,并通过回调函数的形式消费共享的数据;
  5. 在根组件中可以通过执行方法更新数据,会触发所有消费数据组件的渲染。

这样,我们就实现了在函数式组件中用Context API共享数据,接下来我们看如何使用useContext。

三、使用useContext消费Context对象中的数据

useContext是React推出的用以消费Context数据的一个hook,使用此hook最大的好处是我们可以无需引入Consumer自定义组件包裹需要消费数据的组件。下面,我们对刚创建的演示组件进行改造。

1. 改造:Home.jsx组件
import React, { useState } from "react";
import FirstChild from "./FirstChild";

// 创建一个共享 theme的 Context对象,并解构出Provider和Consumer对象
export const Global= React.createContext();
export default () => {
  //使用hook创建theme内容
  const [theme, setTheme] = useState({ name: "主题1", color: "red" });
  return (
    //  通过Provider 的value属性注入theme数据
    <Global.Provider value={theme}>
      <div>
        <p>父组件定义的值:{JSON.stringify(theme)}</p>
        <FirstChild />
        <button
          onClick={() => {
            setTheme((oldVal) => {
              return { ...oldVal, name: "主题2" };
            });
          }}
        >
          改变主题
        </button>
      </div>
    </Global.Provider>
  );
};

在前文中提到,使用useContext消费Context数据并不要使用Consumer自定义组件包裹,因此这里我们不再通过解构暴露Provider和Consumer对象,而是暴露一个没有解构的Consumer对象。然后将Provider这个自定义组件的名称改为了Global.Provider

2. 改造:FirstChild.jsx组件
import React, { useContext } from "react";
//引入Context对象的Consumer以消费数据
import { Global } from "./Home.jsx";
import Grandson from "./GrandChild.jsx";
function FirstChild() {
  const themeContext = useContext(Global);
  return (
    <div>
      子组件获取的数据:{JSON.stringify(themeContext)}
      <Grandson />
    </div>
  );
}
export default FirstChild;

在子组件中,引入了名为Global的Context对象,然后使用useContext声明了名为themeContext的对象,并将Global作为初始化的参数。这时候themeContext就保存了Global对象中存储的数据,就可以很方便的在组件中使用所有共享数据。

3. 改造:GrandChild.jsx组件
import React, { useContext } from "react";
import { Global } from "./Home"; //引入父组件的Consumer容器
const GrandChild = (props) => {
  const themeContext = useContext(Global);
  return (
    //Consumer容器,可以拿到上文传递下来的name属性,并可以展示对应的值
    <div>
      <p>孙子组件获取的数据{JSON.stringify(themeContext)}</p>
    </div>
  );
};
export default GrandChild;

在孙子组件中,同样使用useContext对象创建了名为themeContext的对象,成功拿到了Context中的数据。

四、总结

本文主要介绍了React Context API的使用方式。使用React开发中小型项目,React Context完全能够满足我们的开发需求,如果引入了Redux这一类的状态管理库会显得过于笨重,会增加打包后的体积,不利于项目的后期优化。官方推出useContext hook大家也一定要掌握,可以帮助我们节约很多代码量。