背景
在 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 的使用,并在子组件中修改 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 对象的 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> </> }- 效果示例
- 注意事项
- 由示例中可见,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]) }- 效果示例
- demo 说明:
* 示例项目目录结构图
杂谈
- 本文的主要场景大部分借鉴于官网文档,所以强烈建议大家在对 API 有不理解的地方时,优先阅读官方文档。
- 如果发现使用的 API 与示例中有所出入,可以参考官网中列举的过时的 API,详情见参考文章,本文不对之前的 context 做过多说明。
- 本文主要目的有两个,一个是说明 class 和 hook 中 context 的用法,另一个是想说明一件事,就是如果有更好的方案,则不要优先考虑使用 context,如果用了也不要觉得会产生很大的问题,因为已经有很成熟的三方库也在使用这一特性。
- 本文不对文中出现的 react-redux、react-router 等三方库做过多说明,也不会对 useMemo 这类缓存优化手段做过多说明,同时也不对 Context 的原理做深入讲解。
- 本来只是想说明一个 API 的使用,但最终除了说明 API 的使用外,更多的想分享给大家的是一种思考方式,如何去设计一个个 demo 示例,如何去发现一些 API 在优秀的三方库的应用,从而去深入原理、学习三方库的一连串思维方式。
参考文章
- React 官方文档:react.docschina.org/docs/contex…
- React 官方文档-过时的 context 文档:react.docschina.org/docs/legacy…
- React 官方文档-useContext 文档:react.docschina.org/docs/hooks-…
- React-Router 源码:github.com/remix-run/r…
- React-Redux 源码:github.com/reduxjs/rea…
浏览知识共享许可协议
本作品采用知识共享署名-相同方式共享 4.0 国际许可协议进行许可。