这样使用React Context,提高你的工作效率

612 阅读6分钟

写在前面

不管你是写React的一两年的程序员老司机,还是刚刚接触React的小白程序员,如果你想掌握React并且使用它来解决各种各样的需求,那么你就一定需要掌握React Context。

在我没有掌握Context之前,总是感觉这种技术是多余的,在项目中使用它完全是画蛇添足,甚至可以说是舍近求远。但是当我结合一些具体的业务深入学习它之后——嗯~,Context真香。

Context是什么?该怎么使用?能不能用在class组件和function组件中?有哪些具体的业务场景?

我结合自己的学习心得,在这篇博客中跟大家一一分享。天太热,准备好西瓜,咱们开始吧!

Context是什么

Context直译过来就是“上下文”的意思,我们可以把它理解成代码所处的环境,如果你的一段代码在某一个“上下文”中,那么可以在代码中访问这个“上下文”中的一些变量和方法,即使这些变量和方法没有在你的这段代码中进行定义。

做一个类比,对于一个学生来讲,他的上下文就是学校,那么他可以使用学校图书馆中的资源,即使这些资源不是他自己的。

在React中也是一样,我们可以给一个React组件指定一个上下文,在这个上下文中定义一些变量和方法,那么在这个组件中就可以直接使用这些变量和方法了。

如果我们给所有的组件中都设置成同样的上下文,会怎么样呢?当然是所有的组件就都可以共享同样的一批数据,这就是Redux库所提供的功能。

该怎么使用Context

了解了Context之后,我们要怎么使用Context呢?

我们先假设一个使用场景:如下图所示,项目中有App根组件,它下面有一个Page组件,Page组件中又分别有Header, Body, Footer三个组件。

image.png

这些组件中有不同的内容,项目中要有一个可以一键更改主题颜色的功能。具体场景如下图所示:

这个时候,我们可以使用Context一步步来完成这些需求。如果你想直接查看demo代码,可以点击:codesandbox.io/s/react-con…

首先我们需要定义一个Context对象,在项目中新建ContextData.js文件,代码如下:

import React, { createContext } from "react";
export const ContextData = createContext("light");

这里使用createContext这个api创建的Context对象,在创建的时候,我们需要传给它一个默认值。这个默认值基本没有什么作用,后面在使用的时候,会把它给覆盖掉。不过它也不是一无是处,后面我们会讲到会在什么情况下用到了

Context对象创建之后,如果我们在控制台中把它打印出来,可以看到它有这些属性:

{
  $$typeof: xxxx,
  **Consumer**: xxxx,
  **Provider**: xxxx,
  _calculateChangedBits: null,
  _currentRenderer: {},
  _currentRenderer2: null,
  _currentValue: "light",
  _currentValue2: "light",
  _threadCount: 0
}

要着重介绍ConsumerProvider属性,这两个属性是两个React组件,翻译过来就是“生产者组件”和“消费者组件”。

Consumer生产者组件通常是一个父组件,我们定义的其他组件要放在它的里面,成为它的子节点或后代节点。Consumer组件用来提供共享的数据,我们可以理解成它定义一个上下文,这样一来,他的后代节点就可以使用上下文中的变量和方法了。

Provider消费者节点通常是一个或多个子节点,这些节点可以使用生产者节点提供的上下文中的数据。

Context对象定义之后,我们就可以在其他组件中引入并使用它,我们在App组件中使用它,代码如下:

import React from 'react';
import Page from "./Page.jsx";
import { ContextData } from './ContextData';  // 引入Context对象

class App extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      themeColor: "orange"
    }
  }

  changeThemeColor = (colorName) => {
    this.setState({
      themeColor: colorName
    })
  }

  render() {
	  // 定义上下文中的数据,这里不仅可以定义变量,还可以定义方法
    const contextValue = {
      changeThemeColor: this.changeThemeColor,
      themeColor: this.state.themeColor
    }

    return (
      <ContextData.Provider value={contextValue}>
        <div>
          <p>Please select your theme color</p>
          <button onClick={() => { this.changeThemeColor("red") }}>Red</button>
          <button onClick={() => { this.changeThemeColor("green") }}>Green</button>
          <button onClick={() => { this.changeThemeColor("yellow") }}>Yellow</button>
        </div>
				{/* 这里需要把Page组件包裹进来 */}
        <Page />
      </ContextData.Provider>
    );
  }
}
export default App;

在上述代码中我们可以看到,需要使用<ContextData.Provider value={contextValue}>组件包裹其他组件,这里需要指定value属性,这个属性就是上下文中的数据,它的子元素中所共享的变量就是这个属性。

同时我们也把Page组件作为<ContextData.Provider value={contextValue}>组件的子节点,这样在Page节点及其子结点中使用上下文中的数据了。Page组件的代码如下:

import React, { Component } from "react";
import Header from './Header.jsx';
import Body from './Body.jsx';
import Footer from './Footer.jsx';

class Page extends Component {

  constructor(props) {
    super(props);
  }

  render() {
    return (<>
      <Header />
      <Body />
      <Footer />
    </>);
  }
}

export default Page;

我们可以直接在Page组件中使用上下文的数据,但是这里没有,而是引入了子节点,我们在它的子节点中使用上下文数据是一样的。这里以Header组件为例,代码如下:

import React, { Component } from "react";
import { ContextData } from "./ContextData.js";  // 这里要引入Context对象

class Header extends Component {
	// 这一行代码很重要,通过class的静态属性contextType,我们可以把上下文数据绑定到该class实例的context属性
  static contextType = ContextData; 

  constructor(props) {
    super(props);
  }
  
  render() {
		// 通过this.context来使用上下文中的数据
    return <div style={{ backgroundColor: this.context.themeColor }}>
      This is the Header component, the whole theme color is {this.context.themeColor}
    </div>
  }
}

export default Header;

Header是class形式定义的组件,但是现在越来越推崇使用function组件,那么该如何在function组件中使用上下文中的变量呢?这里我们可以参考Body组件中的代码:

import React from "react";
import { ContextData } from "./ContextData.js";  // 这里要引入Context对象

const Body = () => {
	// 这里需要使用消费者组件包裹,并且需要一个函数作为子元素
  return <ContextData.Consumer>
    {
      contextValue => {
        const { themeColor, changeThemeColor } = contextValue;
        return  <button
          onClick={() => { changeThemeColor("blue") }}
          style={{ backgroundColor: themeColor }} >
          Blue Color
        </button>
    }
  }
  </ContextData.Consumer>;
}

export default Body;

function组件中没有静态属性contextType,需要使用消费者组件<ContextData.Consumer>结合一个函数的形式来获取上下文中的数据,并且这个函数要返回一个React节点。

传递给函数的value值等价于组件树上方离这个context最近的 Provider 提供的 value 值。如果没有对应的Provider,value参数等同于传递给createContext()的defaultValue。

我们还可以看到一个有趣的事情,在Body组件中,我们不仅可以使用上下文问中的变量,还可以使用上下文中的方法,来修改上下文中的变量,这一点可以支持在后代节点中,修改祖先节点中的数据。

在function组件中,除了上述使用<ContextData.Consumer>之外,还有另外一种方式上下文数据的方式,那就是使用useContext这一钩子,我们可以参考Footer组件中的代码:

import React, { useContext } from "react";
import { ContextData } from "./ContextData.js";  // 这里要引入Context对象

const Footer = () => {
  const context = useContext(ContextData);  // 把整个context对象传给useContext 

	// 然后就可以直接访问上下文中的变量和方法了
  const handleChangeThemeColor = () => {
    context.changeThemeColor("gray")
  }

  return <div
    onClick={handleChangeThemeColor}
    style={{ color: context.themeColor }}>
    This is the footer text
  </div>
}

export default Footer;

两种方式比较下来,我比较推荐第二种,写法更简洁,而且现在React官方也是比较鼓励使用Hooks。

写在最后

关于Context的学习心得就分享结束了,如果你想了解更多,可以查看React的官方文档,写得还是非常清楚的:

Context文档:zh-hans.reactjs.org/docs/contex…

useContext文档: zh-hans.reactjs.org/docs/hooks-…