React框架 | 超详细的 Context API 使用指南

2,189 阅读12分钟

背景

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

最近在研究 react 相关的 hook,其中就有说到 useContext 的应用,索性把 context 的整个 API 都梳理了一遍。

本文使用的 react 版本基于 v17.0.2。

目录

  • class 组件中 context 的用法
  • 函数组件 hook 中 useContext 的用法
  • react-redux 中 context 的应用
  • react-router 中 context 的应用
  • 项目中如何使用 context

class 组件中 context 用法

class 组件中使用 context,主要关注如何创建 context 对象,以及其返回的 Provider 容器和 Consumer 组件的使用方式。

API 说明
  • React.createContext :创建一个 Context 对象。当 React 渲染一个订阅了这个 Context 对象的组件,这个组件会从组件树中离自身最近的那个匹配的 Provider 中读取到当前的 context 值。
  • Context.Provider:每个 Context 对象都会返回一个 Provider React 组件,它允许消费组件订阅 context 的变化。当 Provider 的 value 值发生变化时,它内部的所有消费组件都会重新渲染。
  • Context.Consumer:每个 Context 对象都会返回一个 Consumer React 组件,可以订阅 context 的变更,此组件可以让你在函数式组件中可以订阅 context。
  • Class.contextType:挂载在 class 上的 contextType 属性可以赋值为由 React.createContext() 创建的 Context 对象。此属性可以让你使用 this.context 来获取最近 Context 上的值。你可以在任何生命周期中访问到它,包括 render 函数中。
  • Context.displayName:context 对象接受一个名为 displayName 的 property,类型为字符串。React DevTools 使用该字符串来确定 context 要显示的内容。
用法示例
  • 示例一:同一个 context 在跨组件、隔代组件中的使用

    • 入口文件代码示例,index.js
    // demo 项目入口文件
    import ClassContext from "./classContext";
    export default function ContextComponent () {
        return <div style={{border: '1px solid #999', padding: '10px'}}>
            我是 context 组件
            <ClassContext />
        </div>
    }
    
    • classContext 代码示例,/classContext/index.js
    import React, {Component} from "react";
    import Parent from './parent'
    
    export default class ClassContext extends Component {
        render() {
            return <div style={{border: '1px solid #999', padding: '10px'}}>
                我是 class context 组件
                <Parent />
            </div>
        }
    }
    
    • Context 代码示例
    // context.js 文件,用于初始化 Context 对象
    import React from 'react'
    export const ColorContext = React.createContext('red')
    export const NameContext = React.createContext('zhangsan')
    
    • Provider 代码示例
    // parent.js 文件
    import {Component} from "react";
    import Child1 from './child-1'
    import Child2 from './child-2'
    import {ColorContext} from './context'
    
    export default class Parent extends Component {
        render() {
            return <>
                <ColorContext.Provider value="green">
                    <h1>我是父组件</h1>
                    {/* 孙子组件中引用数据 */}
                    <Child1 />
                    {/* 子组件中引用数据 */}
                    <Child2 />
                </ColorContext.Provider>
            </>
        }
    }
    
    • Consumer 代码示例
    // child-1.js
    import {Component} from "react";
    import {ColorContext, NameContext} from './context'
    import Son from './child-1-son'
    
    
    export default class Child extends Component {
        // 此处是使用了 class 组件中针对 context 的 contextType API
        static contextType = NameContext
        componentDidMount() {
            // 此时打印的 context 为 NameContext 中的默认值
            console.log(this.context)
        }
    
        render() {
            return <>
             <h3>我是子组件1:</h3>
             <Son />
        </>
        }
    }
    
    
    // child-1-son.js ,用于说明 context 是可以跨组件传值
    import {Component} from "react";
    import {ColorContext, NameContext} from './context'
    
    
    export default class Child extends Component {
        static contextType = NameContext
        componentDidMount() {
            console.log(this.context)
        }
    
        render() {
            return <>
             <h3>我是孙子组件:</h3>
            <ColorContext.Consumer>
                {value => {
                    return <>context data: {value}</>
                }}
            </ColorContext.Consumer>
        </>
        }
    }
    
    // child-2.js ,与 child-1.js 导出的对象同级,用于说明同级别的子组件也可以获取到 context 传递的值
    import {Component} from "react";
    import { ColorContext, NameContext} from './context'
    
    
    export default class Child extends Component {
        static contextType = NameContext
        componentDidMount() {
            console.log(this.context)
        }
    
        render() {
            return <>
             <h3>我是子组件2:</h3>
            <ColorContext.Consumer>
                {value => {
                    return <>context data: {value}</>
                }}
            </ColorContext.Consumer>
        </>
        }
    }    
    
    • 效果示例

context的使用-图1-1.jpg

  • 示例二:动态 context 的使用,并在子组件中修改 context 值,方式1和方式2的核心原理是一样的,都是通过动态设置 Provider 的 value 值

    • 方式1:通过改变父级元素中 Provider 组件的动态 value 进行改变

      • Context 的初始化同示例二,不做改变
      • Provider 组件代码示例
      // parent.js 文件
      // 主要关注 NameContext.Provider 中的动态 value 赋值,以及其子自己的 props 入参 changeName 属性
      import {Component} from "react";
      import Child1 from './child-1'
      import Child2 from './child-2'
      import Child3 from './child-3'
      import {ColorContext, NameContext} from './context'
      
      export default class Parent extends Component {
          constructor(props){
              super(props)
              this.state = {
                  userName: '张三'
              }
          }
      
          changeContextName = () => {
              this.setState({
                  userName: '王二_' + Date.now()
              })
          }
      
          render() {
              return <>
                  <ColorContext.Provider value="green">
                      <h1>我是父组件,使用 ColoerContext</h1>
                      {/* 孙子组件中引用数据 */}
                      <Child1 />
                      {/* 子组件中引用数据 */}
                      <Child2 />
                  </ColorContext.Provider>
                  <NameContext.Provider value={this.state.userName}>
                      <h1>我是父组件,使用 NameContext</h1>
                      <Child3 changeName={this.changeContextName}/>
                  </NameContext.Provider>
              </>
          }
      }
      
      
      • Consumer 组件代码示例
      // child-3.js 文件
      // 主要关注 NameContext.Consumer 中的按钮点击事件
      import {Component} from "react";
      import { NameContext} from './context'
      
      
      export default class Child extends Component {
      
          render() {
              return <>
               <h3>我是子组件3:</h3>
              <NameContext.Consumer>
                  {(contextValue)=> {
                      return <div>
                      context data: {contextValue}
                      <button onClick={this.props.changeName} style={{marginLeft: '10px'}}>点我变名字</button>
                      </div>
                  }}
              </NameContext.Consumer>
          </>
          }
      }
      
    • 方式2:通过在 Context 初始化时,创建一个用于修改的函数属性,进而动态改变父级元素的 Provider 组件的动态 value 进行改变

      • Provider 组件代码示例
      // parent.js 文件
      // 重点关注 NameContext 相关的内容,子组件3 中会用到 Provider 传入的 changeName 方法
      import {Component} from "react";
      import Child1 from './child-1'
      import Child2 from './child-2'
      import Child3 from './child-3'
      import {ColorContext, NameContext} from './context'
      
      export default class Parent extends Component {
          constructor(props){
              super(props)
              this.state = {
                  userName: '张三'
              }
          }
      
          changeContextName = () => {
              this.setState({
                  userName: '王二_' + Date.now()
              })
          }
      
          render() {
              return <>
                  <ColorContext.Provider value="green">
                      <h1>我是父组件,使用 ColoerContext</h1>
                      {/* 孙子组件中引用数据 */}
                      <Child1 />
                      {/* 子组件中引用数据 */}
                      <Child2 />
                  </ColorContext.Provider>
                  <NameContext.Provider value={{
                      userName: this.state.userName,
                      changeName: this.changeContextName
                  }}>
                      <h1>我是父组件,使用 NameContext</h1>
                      <Child3 />
                  </NameContext.Provider>
              </>
          }
      }
      
      
      • Consumer 组件代码示例
      // child-3.js 文件,主要关注 contextValue 值的结构,按钮的 click 事件是使用的 Provider 组件传递过来的 changeName 方法
      import {Component} from "react";
      import { NameContext} from './context'
      
      
      export default class Child extends Component {
      
          render() {
              return <>
               <h3>我是子组件3:</h3>
              <NameContext.Consumer>
                  {(contextValue)=> {
                      const {userName, changeName} = contextValue
                      return <div>
                      context data: {userName}
                      <button onClick={changeName} style={{marginLeft: '10px'}}>点我变名字</button>
                      </div>
                  }}
              </NameContext.Consumer>
          </>
          }
      }
      
      • Context 中的代码示例
      // context.js 文件
      // 主要关注 NameContext 对象的初始值,changeName 是为了方便 Provider 的赋值及 Consumer 的使用
      import React from 'react'
      export const ColorContext = React.createContext('red')
      export const NameContext = React.createContext({
          userName: 'zhangsan',
          changeName: ()=>{}
      })
      
  • 示例三:同时存在多个 context 时,获取对应的数据

    • Context 的初始化同示例二,不做改变
    • Provider 组件代码示例
    // parent.js
    // 重点关注 ColorContext.Provider 和 NameContext.Provider 的嵌套
    import {Component} from "react";
    import Child1 from './child-1'
    import Child2 from './child-2'
    import {ColorContext, NameContext} from './context'
    
    export default class Parent extends Component {
    
        render() {
            return <>
                <ColorContext.Provider value="green">
                    <h1>我是父组件,使用 ColoerContext</h1>
                    {/* 孙子组件中引用数据 */}
                    <Child1 />
                    {/* 子组件中引用数据 */}
                    <NameContext.Provider value="张三">
                        <Child2 />
                    </NameContext.Provider>
                </ColorContext.Provider>
            </>
        }
    }
    
    • Consumer 组件代码示例
    // child-2.js 文件
    // 重点关注 ColorContext.Consumer 和 NameContext.Consumer 的嵌套
    import {Component} from "react";
    import { ColorContext, NameContext} from './context'
    
    
    export default class Child extends Component {
    
        render() {
            return <>
             <h3>我是子组件2:</h3>
            <ColorContext.Consumer>
                {colorContextValue => {
                    return <NameContext.Consumer>
                        {
                            nameContextValue => {
                                return <>
                                    <div>name data: {nameContextValue}</div>
                                    <div>color data: {colorContextValue}</div>
                                </>
                            }
                        }
                    </NameContext.Consumer>
                }}
            </ColorContext.Consumer>
        </>
        }
    }
    
    
    • 效果示例:

context的使用-图1-2.jpg

注意事项
  • 如果 context 对象的 Provider 和 Consumer 不是嵌套的层级关系,而是相同层级关系,则出现的结果是 Consumer 中拿到的是 context 初始值,而不是 Provider 中设置的值。
  • 上一项也可以这样解释:当消费组件没有匹配到 Provider , Context 对象的 defaultValue 参数生效(注意:将 undefined 传递给 Provider 的 value 时,defaultValue 不会生效)

函数组件 hook 中 useContext 的用法

函数组件的 hook 中使用 useContext ,Context 的 Provider 还是需要像 Class 中一样在父元素中创建 Provider 组件,只不过子元素的 Context 可以不用 Consumer 单独声明,而直接通过 useContext 即可获取到父元素设置的 Context 值。

  • API 说明

只有一个 API ,即 useContext,使用的注意事项是, useContext(context) ,入参 context 是 createContext 方法返回的 context 对象,而出参是 Consumer 值,可以直接使用,而不需要再渲染一个函数式组件。

  • 用法示例:主要是对 useContext 返回的 Consumer 对象的处理,其余用法与 class 组件大同小异,因此不单独做过多说明,就统一列举一下比较完整的示例,并在代码中加以注释。

    • 入口文件示例
    // 重点关注 HookContext 组件
    import ClassContext from "./classContext";
    import HookContext from './hookContext'
    export default function ContextComponent () {
        return <>
        <div style={{border: '1px solid #999', padding: '10px'}}>
            我是 context 组件
            <ClassContext />
            <HookContext />
        </div>
        </>
    }
    
    • hookContext 代码示例
    import React from 'react'
    import Parent from './parent'
    
    export default function HookContext () {
        return <div style={{border: '1px solid #999', padding: '10px'}}>
                我是 hook context 组件
                <Parent />
            </div>
    }
    
    • Context 代码示例
    // context.js 文件
    // 通过代码可以看出,与 class 中的 context 并无差别
    import React from 'react'
    export const ColorContext  = React.createContext('red')
    export const NameContext = React.createContext({
        userName: 'zhangsan',
        changeName: () => {}
    })
    
    • Provider 组件示例
    // parent.js 文件
    // 重点关注 ColorContext 和 NameContext 的嵌套用法
    // 重点关注 NameContext 中传入 changeName 方法属性的用法
    import React, {useState} from 'react'
    import Child from './child'
    import { ColorContext, NameContext } from './context'
    
    export default function Parent () {
        const [userName, setUserName] = useState('张三')
        
        const changeName = () =>{
            setUserName('王二')
        }
        
       return <div>
        <h1>我是 Hook Context 父组件,使用 ColoerContext</h1>
        <ColorContext.Provider value='blue'>
            <NameContext.Provider value={{
                userName,
                changeName
            }}>
            <Child />
            </NameContext.Provider>
            </ColorContext.Provider>
        </div>
    }
    
    • Consumer 组件示例
    // child.js 文件
    // 重点关注 useContext 的入参和出参
    // 重点关注 ColorContext 和 NameContext 值的获取和用法
    import React, { useContext } from 'react'
    import { ColorContext, NameContext } from './context'
    
    export default function Child(){
        const colorContext = useContext(ColorContext)
        const nameContext = useContext(NameContext)
        return <>
            <h3>我是子组件:</h3>
            <p>color data: {colorContext}</p>
            <p>name data: {nameContext.userName}</p>
            <div><button onClick={nameContext.changeName}>点我切换名字</button></div>
        </>
    }
    
    • 效果示例

context的使用-图2-1.jpg

  • 注意事项
    • 由示例中可见,useContext 在嵌套 Context 时,只需要嵌套 Provdier 组件,并在子组件中多次 useContext 即可,而无需做额外嵌套
    • 动态修改 Context 的值,用法与 class 中的方式并无明显差异,也是通过 props 或 context 中设置方法属性来修改 Provdier 中的 value 属性
    • 可以理解为 useContext 相当于 class 组件中的 static contextType = MyContext 或者 <MyContext.Consumer>

react-redux 中 context 的应用

本文不会介绍 react-redux 的使用,只是介绍 context 在 react-redux 中的应用。

下载 react-redux 源码后,搜索关键词 Provider 和 Consumer 即可看到应用的地方。后续有时间再考虑针对 react-redux 本身的原理做深入分析。

  • Provider 应用,./react-redux/src/components/Provider.tsx
// 在 react-redux 中常用的 API 组件,Provider 即是对 context 的 Provider 做了一层封装的高阶组件,其涉及 context 的源码示例如下:

// ... 省略部分代码
function Provider<A extends Action = AnyAction>({
  store,
  context,
  children,
  serverState,
}: ProviderProps<A>) {
  const contextValue = useMemo(() => {
    const subscription = createSubscription(store)
    return {
      store,
      subscription,
      getServerState: serverState ? () => serverState : undefined,
    }
  }, [store, serverState])

  // ... 省略部分代码


  // 此处可以看到 react-redux 的 Provider 组件的原理即 Context.Provider
  return <Context.Provider value={contextValue}>{children}</Context.Provider>
}

export default Provider

  • Consumer 应用,./react-redux/src/components/connect.tsx
// 在 react-redux 中常用的 connect API,即是对 context 的 Consumer 做了一层封装的高阶组件,涉及 context 的源码示例如下:

// ... 省略部分代码

const ContextToUse: ReactReduxContextInstance = useMemo(() => {
        // Users may optionally pass in a custom context instance to use instead of our ReactReduxContext.
        // Memoize the check that determines which context instance we should use.
        return propsContext &&
          propsContext.Consumer &&
          // @ts-ignore
          isContextConsumer(<propsContext.Consumer />)
          ? propsContext
          : Context
      }, [propsContext, Context])
      
// ... 省略部分代码

  • 其他说明:在 react-redux 中,除了使用 Consumer 方式消费 context,还需要关注利用 hooks 的 useContext 消费 context 的方式

react-router 中 context 的应用

本文不会介绍 react-router 的使用,只是介绍 context 在 react-router 中的应用。

下载 react-router 源码后,搜索关键词 Provider 和 Consumer 即可看到应用的地方。后续有时间再考虑针对 react-router 本身的原理做深入分析。

  • 在 react-router 中,没有像 react-reudx 的 Consumer 消费 context 的方式,源码中基本都是通过 useContext 的 hook 方式消费 context ,需要特殊注意。

  • Provider 应用,./react-router/packages/react-router/lib/components.tsx


// ... 省略部分代码

export function Router({
  basename: basenameProp = "/",
  children = null,
  location: locationProp,
  navigationType = NavigationType.Pop,
  navigator,
  static: staticProp = false,
}: RouterProps): React.ReactElement | null {

// ... 省略部分代码

  return (
    <NavigationContext.Provider value={navigationContext}>
      <LocationContext.Provider
        children={children}
        value={{ location, navigationType }}
      />
    </NavigationContext.Provider>
  );
}

// ... 省略部分代码

  • useContext 应用
// 在 react-router v6 的版本中,没有见到 class 的方式使用 context 的 Consumer 的调用
// 换了思路之后,发现基本都是 useContext 的 hook 方式获取 context 的 Consumer 的值

// ... 省略部分代码

export function useRoutes(
  routes: RouteObject[],
  locationArg?: Partial<Location> | string
): React.ReactElement | null {
  invariant(
    useInRouterContext(),
    // TODO: This error is probably because they somehow have 2 versions of the
    // router loaded. We can help them understand how to avoid that.
    `useRoutes() may be used only in the context of a <Router> component.`
  );
  // ... 省略部分代码
  let { matches: parentMatches } = React.useContext(RouteContext);
  let routeMatch = parentMatches[parentMatches.length - 1];
    
  // ... 省略部分代码 
}

// ... 省略部分代码

项目中如何使用 context

  • 首先还是不推荐 context 为项目中的首选项,建议多看看是否有别的方案代替
  • 如果真的选择了 context ,也不用太担心,因为上述中的比较成熟的两个 react 库都正常使用了 context
  • 最后如果在项目中使用了 context,将 context 单独提取为一个目录或文件,类似 react-redux,react-router 在 react 项目中会单独创建对应的 store 、router 目录一样,不建议单独写在某个类或方法组件中
  • 结合 memoization 手段进行缓存优化,减少或避免重复渲染,代码示例如下,仅使用 useMemo 进行一个 demo 说明
    • demo 说明:
      • 改造上述 hook 示例中的部分代码
      • 改变组件1中 name 数据时,不影响组件2的 color 数据,影响组件1中的 name 和 color 数据
      • 改变组件2中 color 数据时,既影响组件2的 color 数据,影响组件1中的 name 和 color 数据
      • 结论:通过对组件2进行 useMemo 优化,可以避免改变组件1的 name 数据时,组件2的 color 数据无意义的渲染
    • 其余文件内容均相同,重点关注如下文件的内容
    • context.js
    // 拓展了 ColorContext 的数据,changeColor 用于改变 color 值
    import React from 'react'
    
    export const ColorContext  = React.createContext({
        color: 'red',
        changeColor: () => {}
    })
    export const NameContext = React.createContext({
        userName: 'zhangsan',
        changeName: () => {}
    })
    
    • parent.js
    // 重点关注 ColorContext.Provider 组件的 value 赋值
    import React, {useState, useMemo, useContext} from 'react'
    import Child from './child'
    import Child2 from './child-2'
    import { ColorContext, NameContext } from './context'
    
    export default function Parent () {
        const [userName, setUserName] = useState('张三')
        const [color, setColor] = useState('红色')
    
        const changeName = () =>{
            setUserName('王二 '+ Date.now())
        }
    
        const changeColor = () => {
            setColor('蓝色 ' + Date.now())
        }
    
        return <>
        <h1>我是 Hook Context 父组件,使用 ColoerContext</h1>
        <ColorContext.Provider value={{color, changeColor}}>
            <NameContext.Provider value={{
                userName,
                changeName
            }}>
            <Child />
            </NameContext.Provider>
            <Child2 />
        </ColorContext.Provider>
        
        </>
    }
    
    • child.js
    // 主要是添加了 Date.now() 值的渲染,需要注意的是,不要仅关注 colorContext.color 中的时间戳
    // 对于一个组件来说,只要后面的 Date.now() 渲染了,就说明该组件渲染了
    import React, { useContext } from 'react'
    import { ColorContext, NameContext } from './context'
    
    export default function Child(){
        const colorContext = useContext(ColorContext)
        const nameContext = useContext(NameContext)
    
        return <>
            <h3>我是子组件:</h3>
            <p>color data: {colorContext.color + ',' + Date.now()}</p>
            <p>name data: {nameContext.userName + ',' + Date.now()}</p>
            <div><button onClick={nameContext.changeName}>点我切换名字</button></div>
        </>
    }
    
    • child-2.js
    // 注意本示例只是简单的 useMemo 应用
    // 可以试试 useMemo 中第二个参数改为 colorContext 的话,会出现什么变化,为什么,又如何解决这个问题
    import React, { useContext, useMemo } from 'react'
    import { ColorContext } from './context'
    
    export default function Child(){
        const colorContext = useContext(ColorContext)
        console.log(colorContext.color)
        return useMemo(()=>{
            return <>
            <h3>我是子组件2:</h3>
            <p>color data: {colorContext.color + ',' + Date.now()}</p>
            <div>
                <button onClick={colorContext.changeColor}>点我切换颜色</button>
            </div>
         </>
        },[colorContext.color])
    }
    
    • 效果示例

context的使用-图3-1.gif * 示例项目目录结构图

context的使用-图3-2.gif

杂谈

  • 本文的主要场景大部分借鉴于官网文档,所以强烈建议大家在对 API 有不理解的地方时,优先阅读官方文档。
  • 如果发现使用的 API 与示例中有所出入,可以参考官网中列举的过时的 API,详情见参考文章,本文不对之前的 context 做过多说明。
  • 本文主要目的有两个,一个是说明 class 和 hook 中 context 的用法,另一个是想说明一件事,就是如果有更好的方案,则不要优先考虑使用 context,如果用了也不要觉得会产生很大的问题,因为已经有很成熟的三方库也在使用这一特性。
  • 本文不对文中出现的 react-redux、react-router 等三方库做过多说明,也不会对 useMemo 这类缓存优化手段做过多说明,同时也不对 Context 的原理做深入讲解。
  • 本来只是想说明一个 API 的使用,但最终除了说明 API 的使用外,更多的想分享给大家的是一种思考方式,如何去设计一个个 demo 示例,如何去发现一些 API 在优秀的三方库的应用,从而去深入原理、学习三方库的一连串思维方式。

参考文章

浏览知识共享许可协议

知识共享许可协议
本作品采用知识共享署名-相同方式共享 4.0 国际许可协议进行许可。