一文弄懂 React HOC

news2024/12/25 9:01:16

1. 提出问题

1.HOC 能解决什么问题?
2.HOC 的使用场景?

2. HOC 能解决什么问题?

1.拦截组件渲染,包括是否渲染组件、懒加载组件
2.往组件的 props 中混入所需的东西,比如给非 Route 组件的 props 混入 history 对象,使其能够支持路由跳转
3.监控组件内部状态,如添加额外生命周期、对组件某些事件进行监听等

3. 两种高阶组件 – 属性代理和反向继承

3.1. 属性代理

将原始组件包裹在代理组件中进行增加,并且原始组件是在代理组件中进行渲染的,因此代理组件可以掌控对原始组件的控制权限

/** @description 属性代理 */

import React from 'react'

class HOCDemo1 extends React.Component {render(): React.ReactNode {const TargetComponent = HOC(Foo)return <TargetComponent />}
}

const HOC = (Component: typeof React.Component) => {return class WrappedComponent extends React.Component {render(): React.ReactNode {// 对原始组件的强化操作 -- 混入 propsconst fooEnhancedProps: FooEnhancedProps = {name: 'foo',}// 原始组件在代理组件中渲染return <Component {...fooEnhancedProps} />}}
}

type FooRawProps = {}
type FooEnhancedProps = { name: string }
type FooProps = FooRawProps & FooEnhancedProps
class Foo extends React.Component<FooProps> {render(): React.ReactNode {const { name } = this.propsreturn <div>name: {name}</div>}
}

export { HOCDemo1 } 

这里通过属性代理往原始组件的 props 中混入了新的属性

3.1.1. 优点

1.代理组件和原始组件低耦合
2.类组件和函数组件均可使用
3.可以控制原始组件是否渲染
4.可以嵌套使用

3.1.2. 缺点

1.无法直接获取原始组件的状态,需要通过 ref 获取
2.无法直接继承静态属性,需要额外实现或者使用第三方库才行
3.如果需要保持 ref 的正确指向,需要配合 forwardRef 转发 ref 到原始组件上

3.2. 反向继承

返回一个继承自原始组件的组件

/** @description 反向继承 */

import React from 'react'

class HOCDemo2 extends React.Component {render(): React.ReactNode {const TargetComponent = HOC(Foo)return <TargetComponent />}
}

const HOC = (Component: typeof Foo) => {return class WrappedComponent extends Component {state: Readonly<FooState> = {name: 'foo',}}
}

interface FooState {name: string
}
class Foo extends React.Component<{}, FooState> {render(): React.ReactNode {const { name } = this.statereturn <div>name: {name}</div>}
}

export { HOCDemo2 } 

这里通过反向继承修改原始组件的 state 实现了和上面属性代理的 Demo 中相同的效果

3.2.1. 优点

1.能够很方便地获取组件内部的状态,如 state、props、生命周期、事件处理函数等
2.基于 ES6 的继承可以很好地继承静态属性,无需额外实现或者第三方库即可管理静态属性

3.2.2. 缺点

1.代理组件与原始组件高耦合
2.函数组件无法使用
3.嵌套使用有风险,内层组件的生命周期会覆盖外层组件的生命周期

4. HOC 的使用场景

4.1. 强化 props

也就是在原始组件的 props 上混入别的 props 以强化原始组件的功能,比如 React Router 的 withRouter

function withRouter(Component) {const displayName = `withRouter(${Component.displayName || Component.name})`const C = (props) => {/*获取 */const { wrappedComponentRef, ...remainingProps } = propsreturn (<RouterContext.Consumer>{(context) => {return (<Component{...remainingProps} // 组件原始的props{...context} // 存在路由对象的上下文,historylocation 等ref={wrappedComponentRef}/>)}}</RouterContext.Consumer>)}C.displayName = displayNameC.WrappedComponent = Component/* 继承静态属性 */return hoistStatics(C, Component)
}
export default withRouter 

4.2. 劫持控制渲染逻辑

利用反向继承的特点,能够通过 super.render() 调用原始组件的渲染函数完成渲染,并且能够获取和修改渲染后的 React Element

/** @description 劫持控制渲染 */

import React from 'react'

const HOCDemo3: React.FC = () => {const TargetComponent = HOC(Foo)return <TargetComponent />
}

const HOC = (Component: typeof React.Component) => {return class WrappedComponent extends Component {render(): React.ReactNode {// 调用原始组件的 render 方法获取渲染后的 React Elementconst el = super.render()// @ts-ignoreconst rawChildren = el.props.children// 修改 elconst modifiedChildren = React.Children.map(rawChildren, (child, idx) => {if (idx === 0) {return <li>React 666</li>}return child})// @ts-ignorereturn React.cloneElement(el, el.props, modifiedChildren)}}
}

class Foo extends React.Component {render(): React.ReactNode {return (<ul><li>React</li><li>Vue</li><li>Solid</li><li>Svelte</li></ul>)}
}

export { HOCDemo3 } 

4.3. 动态加载组件

/** @description 动态加载组件 */

import React, { useEffect, useState } from 'react'

const HOCDemo4: React.FC = () => {const TargetComponent = DynamicLoadHOC(() => {return new Promise((resolve) => {setTimeout(() => {resolve(import('./component'))}, 5000)})})return (<div><TargetComponent /></div>)
}

interface WrappedComponentState {Component: typeof React.Component | null
}
const DynamicLoadHOC = (loader: () => Promise<any>) => {return class WrappedComponent extends React.Component<{},WrappedComponentState> {state: Readonly<WrappedComponentState> = {Component: null,}componentDidMount(): void {if (this.state.Component) returnloader().then((module) => module.default).then((Component) => this.setState({ Component }))}render(): React.ReactNode {const { Component } = this.statereturn Component ? <Component {...this.props} /> : <Loading />}}
}

const Loading: React.FC = () => {return <div>Loading...</div>
}

export { HOCDemo4 } 

4.4. 利用装饰器模式监听组件事件

使用一个外部容器元素包裹原始组件,给这个外部容器元素添加相应事件监听器,本质上就是利用事件代理机制起到一个监听原始组件相应事件的作用

import React from 'react'

import { createLoggerWithScope } from '~/utils'

const logger = createLoggerWithScope('HOCDemo5')

const OnClickHOC = (Component: typeof React.Component) => {class WrappedComponent extends React.Component {handleClick() {logger.log('检测到 click 事件触发')}render(): React.ReactNode {return (<div onClick={() => this.handleClick()}><Component {...this.props} /></div>)}}return WrappedComponent
}

@OnClickHOC
class HOCDemo5 extends React.Component {render(): React.ReactNode {return <button>Button</button>}
}

export { HOCDemo5 } 

可以看到,HOCDemo5 组件中并没有添加事件监听器,但是只要加上 OnClickHOC 装饰器,就可以监听到它的点击事件触发,十分方便!

5. 如何处理静态属性丢失问题?

考虑一下下面这个场景,我们的原始组件上有静态属性和方法,但是 HOC 返回的 WrappedComponent 中是不存在原始组件的静态属性和方法的,这就导致用户在使用了我们的 HOC 后,原来绑定的静态属性和方法莫名其妙丢失了!

/** @description HOC 原始类组件静态属性和方法丢失 */

import React from 'react'

const HOC = (Component: typeof React.Component) => {class WrappedComponent extends React.Component {render(): React.ReactNode {return <Component />}}return WrappedComponent
}

const HOCDemo6 = () => {const TargetComponent = HOC(Foo)// @ts-ignoreconsole.log(TargetComponent.age)// @ts-ignoreTargetComponent.sayHello()return <TargetComponent />
}

class Foo extends React.Component {static age = 21static sayHello() {console.log('hello')}render(): React.ReactNode {return <div>Foo</div>}
}

export { HOCDemo6 } 

那么该如何解决呢?一个很自然的想法是手动将原始组件上的静态属性引用拷贝到 WrappedComponent

const HOC = (Component: typeof React.Component) => {class WrappedComponent extends React.Component {render(): React.ReactNode {return <Component />}}// @ts-ignoreWrappedComponent.age = Component.age// @ts-ignoreWrappedComponent.sayHello = Component.sayHelloreturn WrappedComponent
} 

但这样子其实不太合理,毕竟我们不可能知道用户在使用我们的 HOC 时传入的原始类组件上有什么静态属性和方法,这里推荐使用一个名为 hoist-non-react-statics 的库,它可以帮我们拷贝一个类的静态属性和方法到另一个类上

pnpm i hoist-non-react-statics
pnpm i @types/hoist-non-react-statics -D 
const HOC = (Component: typeof React.Component) => {class WrappedComponent extends React.Component {render(): React.ReactNode {return <Component />}}// 拷贝 Component 静态属性和方法到 WrappedComponent 上hoistNonReactStatics(WrappedComponent, Component)return WrappedComponent
} 

6. 总结

本篇文章我们学习到了:

1.为什么要用 HOC,HOC 解决了什么问题
2.HOC 的两种使用方式 – 属性代理和反向继承,并分别介绍了它们的优缺点
3.介绍了四种 HOC 的使用场景,并通过 Demo 加深理解
4.如何解决 HOC 中原始类组件的静态属性和方法丢失

希望通过本篇文章的学习,能帮助你解决对 HOC 的疑惑,如果有什么别的问题欢迎在评论区留言,我会第一时间回复~

最后

最近还整理一份JavaScript与ES的笔记,一共25个重要的知识点,对每个知识点都进行了讲解和分析。能帮你快速掌握JavaScript与ES的相关知识,提升工作效率。



有需要的小伙伴,可以点击下方卡片领取,无偿分享

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/113961.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

node.js+uni计算机毕设项目交流微信小程序LW(程序+小程序+LW)

该项目含有源码、文档、程序、数据库、配套开发软件、软件安装教程。欢迎交流 项目运行 环境配置&#xff1a; Node.js Vscode Mysql5.7 HBuilderXNavicat11VueExpress。 项目技术&#xff1a; Express框架 Node.js Vue 等等组成&#xff0c;B/S模式 Vscode管理前后端分离等…

信号量和共享内存

信号量 信号量(Semaphore)&#xff0c;有时被称为信号灯&#xff0c;是在多线程环境下使用的一种设施&#xff0c;是可以用来保证两个或多个关键代码段不被并发调用。在进入一个关键代码段之前&#xff0c;线程必须获取一个信号量;一旦该关键代码段完成了&#xff0c;那么该线…

Joplin插件推荐-持续更新

背景 之前因为印象笔记、语雀等笔记软件使用起来都不满足自己的需求&#xff0c;所以后面自己调研后使用了Joplin这个开源笔记软件 &#xff0c;项目主页&#xff1a; https://joplinapp.org 。目前搭建在自己的服务器上。最近发现有很多好用的插件。所以记录分享一下。 总插…

# LowCode 低代码建表工具

LowCode 低代码建表工具 需求描述 将数据库的表映射为实体类&#xff0c;服务启动时&#xff0c;扫描表相关的实体类&#xff0c;根据实体类模型在数据库创建相关的表 依赖 主要依赖&#xff1a;使用 Sprintboot、druid、spring-jdbc、mybatis <!-- https://mvnreposit…

为啥这些开源的网络框架这么强

hi&#xff0c; 大家好&#xff0c;我是大师兄&#xff0c;今天分享一下网络编程下半部分内容&#xff0c;主要分享开源网络io框架用到了哪些核心技术&#xff0c;使他们如此流行&#xff0c;这些技术值得我们学习&#xff0c;可以增加我们编程技巧和优化思路。只有掌握更多技能…

【类和对象(上)】

Quitters never win and winners never quit. 目录 1.面向过程和面向对象初步认识 2.类的引入 3.类的定义 4.类的访问限定符及封装 4.1 访问限定符 4.2 封装 5.类的作用域 6.类的实例化 7.类对象模型 7.1 如何计算类对象的大小 7.2 结构体内存对齐规则 8.this指针 …

理解 Proxy 和 Reflect

03_02_理解 Proxy 和 Reflect 一、开始之前: 为什么还会有这一篇文章呢&#xff1f;不是手写mini-vue吗&#xff1f;其实可以理解成支线任务、番外篇&#xff0c;是对主线内容的补充。 这一篇文章可能文字比较多&#xff0c;理论知识比较多&#xff0c;参考了4本书相关的章节…

九、Express 基本使用(简)

前一篇内容讲到Express框架的安装以及对Express项目的目录文件有一定的认识了解之后&#xff0c;使用Express创建了最基本的一个Web服务器&#xff0c;接下来进行对Express框架的一些内容来做一个基本的使用&#xff1b; 创建 Web 服务器 node 或 nodemon 执行app.js文件&#…

踩坑了、踩到一个特别无语的常识坑

大家好 踩坑了啊&#xff0c;又踩坑了啊&#xff01; 这次踩到一个特别无语的常识坑。知道真相的那一刻&#xff0c;人就是整个麻掉。 先上个代码&#xff1a; private static double calculate(double a, int b) {return a / b; } 复制代码 你先别问为什么计算不用 BigDec…

RxJS初认识

概念&#xff1a; RxJS的运行就是Observable和Observer之间的互动游戏。 Observable就是“可以被观察的对象”&#xff0c;即“可被观察者”&#xff0c;而Observer就是‘观察者’&#xff0c;连接两者的桥梁就是Observable对象的函数subscribe。 RxJS中的数据流就是Observable…

第二十三章 数论——质数(1)(超级详细的推导)

第二十三章 数论——质数一、什么是质数二、质数的判断1、试除法&#xff08;朴素版&#xff09;2、试除法&#xff08;优化版&#xff09;三、分解质因数1、什么是质因数2、算术基本定理3、分解质因数&#xff08;1&#xff09;问题&#xff08;2&#xff09;思路&#xff08;…

RepNAS: 基于NAS的结构重参数化技术

1. 介绍 在过去几年里&#xff0c;NAS技术取得了长足进展。然而&#xff0c;由于搜索约束与实际推理之间的差异导致高效网络搜索仍极具挑战性。为搜索一个具有高性能、低推理延迟的模型&#xff0c;已有方案往往在算法中添加计算复杂度约束。然而&#xff0c;推理速度会受多种…

【强化学习笔记】马尔可夫过程、马尔可夫奖励过程

文章目录1.马尔可夫过程1.1.随机过程1.2.马尔可夫性质1.3.马尔可夫过程2. 马尔可夫奖励过程2.1.回报2.2.价值函数3.马尔可夫决策过程1.马尔可夫过程 马尔可夫过程&#xff08;Markov process&#xff09; 指具有 马尔可夫性质 的 随机过程 &#xff0c;也被称为马尔可夫链&…

C++GUI之wxWidgets(4)-编写应用涉及的类和方法(2)-wxDialog,wxCloseEvent

目录wxDialog包含类继承具体描述模态和无模态支持样式此类发出的事件wxWindow:&#xff1a;Close()wxCloseEvent具体描述使用此类的事件wxDialog 包含 #include <wx/dialog.h>类继承 描述主 具体描述 对话框是一个带有标题栏的窗口&#xff0c;有时还有一个系统菜单…

python-多线程、网络编程、正则表达式

目录 闭包 多线程 主线程 线程阻塞 同步锁 网络编程 正则表达式 re.match函数 re.search方法 re.match与re.search的区别 re.findall()方法 正则表达式的特殊规则 闭包 account0 def atm(num,flag):global accountif flag:accountnumaccountprint(account)else:acco…

免费开源的高精度OCR文本提取,支持 100 多种语言、自动文本定位和脚本检测,几行代码即可实现离线使用(附源码)

免费开源的高精度OCR文本提取,支持 100 多种语言、自动文本定位和脚本检测,几行代码即可实现离线使用(附源码)。 要从图像、照片中提取文本吗?是否刚刚拍了讲义的照片并想将其转换为文本?那么您将需要一个可以通过 OCR(光学字符识别)识别文本的应用程序。 图片文字识…

html圣诞树代码

一、前言 想做一个圣诞树&#xff0c;通过html实现了下 二、效果展示 三、代码 <!DOCTYPE html> <html> <head> <meta http-equiv"Content-Type" content"text/html; charsetutf-8" /> <meta name"viewport" cont…

245. 你能回答这些问题吗——线段树

给定长度为 N 的数列 A&#xff0c;以及 M 条指令&#xff0c;每条指令可能是以下两种之一&#xff1a; 1 x y&#xff0c;查询区间 [x,y] 中的最大连续子段和&#xff0c; 2 x y&#xff0c;把 A[x] 改成 y。 对于每个查询指令&#xff0c;输出一个整数表示答案。 输入格式…

RabbitMQ 第二天 高级 9 RabbitMQ 集群搭建 9.3 集群管理 9.5 负载均衡-HAProxy

RabbitMQ 【黑马程序员RabbitMQ全套教程&#xff0c;rabbitmq消息中间件到实战】 文章目录RabbitMQ第二天 高级9 RabbitMQ 集群搭建9.3 集群管理9.5 负载均衡-HAProxy9.5.1 安装HAProxy9.5.2 配置HAProxy第二天 高级 9 RabbitMQ 集群搭建 9.3 集群管理 rabbitmqctl join_cl…

MariaDB上市:MySQL之父奋斗13年终敲钟 要写代码写到100岁

雷递网 雷建平 12月24日云数据库公司MariaDB日前与特殊目的公司Angel Pond Holdings完成合并&#xff0c;并在纽交所上市&#xff0c;新公司更名为MariaDB。MariaDB是2022年初与Angel Pond Holdings达成合并协议&#xff0c;对新公司的作价为6.72亿美元。MariaDB是MySQL之父Mic…