让你 React 组件水平暴增的 5 个技巧
目录
透传 className、style
通过 forwardRef 暴露一些方法
useCallback、useMemo
用 Context 来跨组件传递值
React.Children、React.cloneElement
总结
最近看了一些 Ant Design 的组件源码,学到一些很实用的技巧,这篇文章来分享一下。
首先,我们用 create-react-app 创建个 React 项目(选择 typescript 模版):
npx create-react-app --template=typescript component-test
进入项目目录,把开发服务跑起来:
npm run start
然后引入 antd:
npm install --save antd
在 App.tsx 里引入几个 antd 组件:
页面上可以看到这俩组件都成功渲染了:
然后我们来看一下 Ant Design 组件里的一些技巧:
透传 className、style
我们可以给组件设置 className 和 style:
import './App.css';
import { Button } from 'antd';function App() {return (<div className="App"><Button className="aaa bbb" style={{width: '100px',height: '50px'}} type="primary">测试</Button></div>);
}export default App;
在页面里打开 DevTools 可以看到 className 和 style 都被设置到了 button 上。
这种功能的实现就是透传 className 和 style 的 props。基本 antd 所有的组件都会做这个。比如 VisualList 组件的源码:
它取了传入的 className、style 的 props,还有剩余的所有 props。对 className 做了一些处理,添加了两个 className:
对 style 也做了扩展,添加了个 position: relative 的样式。
然后把 style、className,额外的 props 都设置给最外层的 div。
这样,使用这个组件的时候,就可以自己定义一些样式,设置一些 props。其中,classnames 是用来动态产生 className 的一个包,用起来很简单。
比如这样调用:
classNames('aaa', { bbb: true, ccc: false }, false, { eee: true });
那么最终的 className 就是 'aaa bbb eee'。
这样,组件用起来体验就和 html 标签差不多,可以自己控制一些样式。这样写 props 的类型的时候,也是直接用了 html 标签的类型。
比如这个 List 的参数就继承了 React.HTMLArrtibutes<any>,也就是任意 html 标签的属性:
当然,children 属性是不可以设置的。因为 React 用 children 参数来传递子组件。比如 form 组件,它的参数是继承了 React.FormHTMLAttributes<HTMLFormElement>:
去掉了 children 和 onSubmit 这俩属性,因为这俩是 From 组件的参数。
也就是说:antd 的组件基本都支持传入 className、style 或者任何 html 标签的 props,会透传 props 到组件内的容器标签,所以用起来体验和原生标签很类似。但这也要求 props 实现 React.FormHTMLAttributes 的 type。
通过 forwardRef 暴露一些方法
外界控制组件的方式就是通过传 props,但有时候想调用组件的一些方法呢?这时候就需要 ref 了。我们先来试一下 ref:
通过 useRef 创建个 ref 对象,然后把 input 标签设置到 ref。在 useEffect 里就可以调用 input 的方法了:
但这是原生标签,如果是组件呢?这时候就需要 forwardRef 了,也就是把组件内的 ref 转发一下。比如这样:
import './App.css';
import { useRef } from 'react';
import { useEffect } from 'react';
import React from 'react';const Guang: React.ForwardRefRenderFunction<HTMLInputElement> = (props, ref) => {return <div><input ref={ref}></input></div>
}const WrapedGuang = React.forwardRef(Guang);function App() {const ref = useRef<HTMLInputElement>(null);useEffect(()=> {console.log('ref', ref.current)ref.current?.focus()}, []);return (<div className="App"><WrapedGuang ref={ref}/></div>);
}export default App;
其实 forwardRef 这个 api 做的事情也很容易懂。就是把 ref 转发到组件内部来设置:
这样就把组件内的 input 通过 ref 的方式传递到了组件外。效果和之前一样:
不过被 forwardRef 包裹的组件的类型就要用 React.forwardRefRenderFunction 了:
第一个类型参数是 ref 的 content 的类型。但有的时候,我不是想把原生标签暴露出去,而是暴露一些自定义方法。这时候就需要 useImperativeHandle 的 hook 了。
这样写:
import './App.css';
import { useRef } from 'react';
import { useEffect } from 'react';
import React from 'react';
import { useImperativeHandle } from 'react';interface RefProps {aaa: () => void;
}const Guang: React.ForwardRefRenderFunction<RefProps> = (props, ref) => {const inputRef = useRef<HTMLInputElement>(null);useImperativeHandle(ref, () => {return {aaa() {inputRef.current?.focus();}}});return <div><input ref={inputRef}></input></div>
}const WrapedGuang = React.forwardRef(Guang);function App() {const ref = useRef<RefProps>(null);useEffect(()=> {console.log('ref', ref.current)ref.current?.aaa();}, []);return (<div className="App"><WrapedGuang ref={ref}/></div>);
}export default App;
也就是用 useImperativeHanlde 自定义了 ref 对象:
小结一下:
React 可以用 ref 保存原生标签,通过 ref.current 调用这个对象的属性、方法。跨组件传递 ref 需要用 forwardRef 方法。如果你要进一步自定义 ref,那就要用 useImperativeHandle 的 hook。
然后看看 antd 组件是怎么用 ref 的。就如说 VisualList 组件:
它也是包了一层 React.forwardRef,内部用 useImperativeHandle 自定义了 ref:
这样外部就可以调用这个 ref 的方法了:
再比如 Form 组件。它也是被 forwarRef 包裹的函数组件:
内部用 useImperativeHandle 返回了自定义的对象:
所以你才可以这样调用 form 组件的方法:
这就是说:antd 的组件都会用 forwardRef 包裹一层,用来转发 ref,或者是转发内部的 html 标签的引用,或者是用 useImperativeHandle 自定义 ref 对象,来暴露一些方法。
useCallback、useMemo
useMemo 和 useCallback 是性能优化相关的 hook。很多人不知道啥时候用,其实看下 antd 怎么用的就知道了:
比如 VisualList 组件里计算 start、end、scrollHeight 这些值需要大量的计算。
这些计算需要每次 render 都跑一遍么?不需要,只有在某些值变化的时候才需要重新计算。
这时候用 React.useMemo 包裹就可以减少计算量,它只会在 deps 数组变化的时候执行第一个参数的函数。
useMemo 是 deps 变化之后重新执行函数创建值,而 useCallback 并不会执行函数,它只是在 deps 变化的时候返回第一个参数的函数:
这样有什么用呢?
react 重新渲染的依据是 props 是否有变化,如果每次都创建新的函数,那是不是每次都会重新渲染?
所以用 useCallback 包裹的函数参数,就可以在 deps 没变的时候,始终返回同一个函数,这样避免了没必要的渲染。
当然,useMemo 也有这个作用。比如说 Form 组件源码里的这个 useMemo:
你说它是为了减少计算量么?并不是,它没有做任何计算,只是把参数原封不动返回了。这也同样是为了避免 props 变化。
也就是说:antd 里很多地方都用了 useMemo 和 useCallback 来进行渲染性能优化。useMemo 只有在 deps 数组变化的时候才会执行第一个函数,返回新的值,可以用来减少不必要的计算,也可以保证 props 不变来避免不要的渲染。useCallback 是只有 deps 数组变化的时候才返回第一个函数的值,可以保证 props 不变来用来避免不必要的渲染
用 Context 来跨组件传递值
antd 里很多配置的传递都是通过 Context。比如 disabled 的设置:
通过 React.createContext 创建 context 对象,通过 Provider 修改 context 的值。
在最外层包裹这个 Provider 组件来修改 context 值:
然后你可以在任意的组件把 context 值取出来用:
像什么主题、大小等配置,都是通过 Context 传递的。除了用来传递配置外,很多组件也依赖 Context 来传递一些值。
比如 Form:
在 Form 组件里设置 form 对象,然后 setFieldValue 设置字段值。
为什么 Form.Item 里加个 name 就可以取出来了呢?我并没有传递 form 参数过去呀?很明显,这里也是用 Context 来传递的。antd 会创建这样一个 context 对象:
然后在外层用 Provider 设置 context 值:
也就是我们这里传的 form:
那 Form.Item 里自然可以拿到 context 的值,从而取到具体字段信息了:
也就是说:antd 里大量用到了 Context,除了用来传递 config、theme、size 等全局配置信息外,还用来跨组件传递数据,比如 Form、Form.Item 组件,就是通过 Provider、useContext 来存取值的。
React.Children、React.cloneElement
React 组件可以设置内容,在组件内通过 props.children 来取。
import React from 'react';interface GuangProps {children: React.ReactNode[];
}const Guang: React.FunctionComponent<GuangProps> = (props) => {console.log(props);return <div className="guang">{props.children}</div>
}function App() {return (<div className="App"><Guang><p>111</p><p>222</p></Guang></div>);
}export default App;
比如我在组件里把 props.children 取出来,放到 className 为 guang 的 div 下:
如果想对这些 children 做一些操作,就需要用 React.Children 的 api 了,比如 React.Children.toArray、React.Children.forEach、React.Children.map。
有同学说,props.children 本来就是数组啊,直接操作不就行了?不行的,直接操作有一些问题,比如我 sort 一下:
会报错:
所以 props.children 不能直接当做数组用,需要 toArray 一下:
这样就没有报错了:
同理,React.Children 的 forEach 和 map 也很容易理解。而且还可以用 React.cloneElement 复制下传入的 ReactElement。比如这样:
用 React.Children.map 遍历 children,对每个 child 复制一份出来,修改下 props ,并且添加一个 children。
效果是这样的:
React.cloneElement 的第二个参数是修改的 props,后面的参数是 children:
结合 React.Children 的 api 和 React.cloneElement 的 api 就可以任意修改 children 渲染的结果。
在 antd 里也有大量运用,比如 button 组件里,通过 map + cloneElement 来处理中文字符的问题:
或者用 map + cloneElement 给 child 的 children 外包一层组件:
更巧妙的是 VirtualList 里的应用:
你不需要给传入的 children 设置 ref,antd 会通过 map + cloneElement 给你加上 ref 的 props,然后在回调函数里把这个 ref 保存下来。
这样就拿到了你传入的每一个 children 的 ref。比如根据 key 来保存每个 Item 的 ref:
也就是说:antd 组件里大量用到了 React.Children + React.cloneElement 的 api 对 props.children 做一些修改,比如包一层组件、添加 ref 等参数、添加一些 children 等。
总结
这篇文章总结了 ant design 组件源码里的 5 个技巧:
-
透传 className、style,还有其他 html 标签的 props,让你的组件用起来体验和原生 html 标签一样
-
通过 forwardRef + useImperativeHandle 暴露一些方法,每个组件都可以通过 ref 暴露一些 api 出来
-
useCallback、useMemo 缓存计算结果,通过让 props 不变来减少没必要的渲染
-
用 Context 的 Provider + useContext 来跨组件传递值,可以用来传递全局配置,也可以用来做业务组件的跨层传递数据
-
通过 React.Children + React.cloneElement 的 api 对 props.children 做各种修改
这些都是在 antd 里随处可见的技巧,可以说任何一个组件里都有这些东西。
这些写 React 组件的技巧你都用过么? 没用过的话不妨从今天开始用起来吧。
相关文章:

让你 React 组件水平暴增的 5 个技巧
目录 透传 className、style 通过 forwardRef 暴露一些方法 useCallback、useMemo 用 Context 来跨组件传递值 React.Children、React.cloneElement 总结 最近看了一些 Ant Design 的组件源码,学到一些很实用的技巧,这篇文章来分享一下。 首先&am…...

阿里云部署 ChatGLM2-6B 与 langchain+ChatGLM
1.ChatGLM2-6B 部署 更新系统 apt-get update 安装git apt-get install git-lfs git init git lfs install 克隆 ChatGLM2-6B 源码 git clone https://github.com/THUDM/ChatGLM2-6B.git 克隆 chatglm2-6b 模型 #进入目录 cd ChatGLM2-6B #创建目录 mkdir model #进入目录 cd m…...

F12开发者工具的简单应用
目录 elements 元素 1、元素的定位和修改 2、UI自动化应用 console 控制台 sources 源代码 network 网络 1、定位问题 2、接口测试 3、弱网测试 performance 性能 memory 存储 application 应用 recorder 记录器 界面展示如下(设置中可以切换中英文&am…...

【 Python 全栈开发 - 人工智能篇 - 45 】决策树与随机森林
文章目录 一、概念与原理1.1 决策树1.1.1 概念1.1.2 原理特征选择分割方法 1.1.3 优点与缺点1.1.4 Python常用决策树算法 1.2 随机森林1.2.1 概念1.2.2 原理1.2.3 优点与缺点1.2.4 Python常用随机森林算法 1.3 决策树与随机森林的比较1.3.1 相同之处1.3.2 不同之处 二、决策树算…...
SpringBoot集成kafka全面实战
本文是SpringBootKafka的实战讲解,如果对kafka的架构原理还不了解的读者,建议先看一下《大白话kafka架构原理》、《秒懂kafka HA(高可用)》两篇文章。 一、生产者实践 普通生产者 带回调的生产者 自定义分区器 kafka事务提交…...

新建Git仓库,将本地文件上传至仓库
1、新建仓库,勾选初始化仓库 2、复制仓库链接 3、打开本地文件目录 右键选择 Git Bash Here 打开命令窗口 4、依次按照下面的步骤(*如果报错,看原目录下是否存在 .git 需要删除) // 生成git文件 git init // 把文件加入暂存区 g…...

算法练习——力扣随笔【LeetCode】【C++】
文章目录 LeetCode 练习随笔力扣上的题目和 OJ题目相比不同之处?定义问题排序问题统计问题其他 LeetCode 练习随笔 做题环境 C 中等题很值,收获挺多的 不会的题看题解,一道题卡1 h ,多来几道,时间上耗不起。 力扣上的题…...

web服务器(Tomcat)
目录 一、web服务器 1. 常见web服务器 2. web服务器简介 二、 Apache Tomcat服务器 1. Tomcat服务器简介 2. Tomcat服务器基本使用 3. 启动tomcat常见问题 (1)启动tomcat控制台乱码 (2)启动tomcat闪退问题 (…...
测试方案、功能测试报告、性能测试报告
测试方案内容概要: 项目内容介绍,测试计划安排(人员时间),测试环境(系统配置)需求功能点(内容介绍,测试安排),重点难点场景,系统集成…...

【代码随想录day21】二叉搜索树的最近公共祖先
题目 思路 解题的关键是知道自顶向低递归遍历,第一次遇到root在p和q的区间中时,则root就是p和q的最近公共祖先节点。 递归法 # Definition for a binary tree node. # class TreeNode: # def __init__(self, x): # self.val x # …...

ssm文章发布管理系统java小说作品发表jsp源代码mysql
本项目为前几天收费帮学妹做的一个项目,Java EE JSP项目,在工作环境中基本使用不到,但是很多学校把这个当作编程入门的项目来做,故分享出本项目供初学者参考。 一、项目描述 ssm文章发布管理系统 系统有2权限:前台账…...

AXI协议之AXILite开发设计(二)
微信公众号上线,搜索公众号小灰灰的FPGA,关注可获取相关源码,定期更新有关FPGA的项目以及开源项目源码,包括但不限于各类检测芯片驱动、低速接口驱动、高速接口驱动、数据信号处理、图像处理以及AXI总线等 二、AXI-Lite关键代码分析 1、时钟与…...

Qgis二次开发-QgsMapTool地图交互工具详解
1.简介 QgsMapTool地图工具是用于操作地图画布的用户交互式工具。例如,地图平移和缩放功能被实现为地图工具。 QgsMapTool是抽象基类,以下是类的继承关系: 2.常用接口 virtual void canvasDoubleClickEvent (QgsMapMouseEvent *e)重写鼠标…...

MySQL基础(四)数据库备份
目录 前言 一、概述 1.数据备份的重要性 2.造成数据丢失的原因 二、备份类型 (一)、物理与逻辑角度 1.物理备份 2.逻辑备份 (二)、数据库备份策略角度 1.完整备份 2.增量备份 三、常见的备份方法 四、备份(…...

子类化QThread来实现多线程,moveToThread函数的作用
子类化QThread来实现多线程, QThread只有run函数是在新线程里的,其他所有函数都在QThread生成的线程里。正确启动线程的方法是调用QThread::start()来启动。 一、步骤 子类化 QThread;重写run,将耗时的事件放到此函数执行&#…...

经典面试题(力扣,接雨水)
接雨水 方法一思路测试代码复杂度测试结果 方法二思路测试代码复杂度测试结果 给定 n 个非负整数表示每个宽度为 1 的柱子的高度图,计算按此排列的柱子,下雨之后能接多少雨水。 示例1: 输入:height [0,1,0,2,1,0,1,3,2,1,2,1]…...

2023年深圳杯数学建模C题无人机协同避障航迹规划
2023年深圳杯数学建模 C题 无人机协同避障航迹规划 原题再现: 平面上A、B两个无人机站分别位于半径为500 m的障碍圆两边直径的延长线上,A站距离圆心1 km,B站距离圆心3.5 km。两架无人机分别从A、B两站同时出发,以恒定速率10 m/s…...

PostgreSQL--实现数据库备份恢复详细教学
前言 这是我在这个网站整理的笔记,关注我,接下来还会持续更新。 作者:RodmaChen PostgreSQL--实现数据库备份恢复详细教学 一. 数据库备份二. 数据库恢复三. 存留问题 数据库备份恢复功能是每个产品所需的,以下是简单的脚本案例&a…...
JDK工具之jstack说明
JDK工具之jstack说明 前言什么是jstack?如何使用jstack?获取Java进程的PID分析jstack输出 常用的jstack命令选项jstack的应用场景结论 前言 作为Java开发人员,在开发和维护复杂的Java应用程序时,我们经常会遇到各种各样的问题&am…...
34 | 牛顿迭代法
文章目录 牛顿迭代法一、原理二、Python实现三、练习题四、总结牛顿迭代法 一、原理 牛顿迭代法(Newton’s Method)是一种用于寻找方程的实根的数值方法。其基本思想是通过一系列逼近来求解方程的根。对于方程 f ( x ) = 0 f(x) = 0 f(x...
后进先出(LIFO)详解
LIFO 是 Last In, First Out 的缩写,中文译为后进先出。这是一种数据结构的工作原则,类似于一摞盘子或一叠书本: 最后放进去的元素最先出来 -想象往筒状容器里放盘子: (1)你放进的最后一个盘子(…...
JVM垃圾回收机制全解析
Java虚拟机(JVM)中的垃圾收集器(Garbage Collector,简称GC)是用于自动管理内存的机制。它负责识别和清除不再被程序使用的对象,从而释放内存空间,避免内存泄漏和内存溢出等问题。垃圾收集器在Ja…...
测试markdown--肇兴
day1: 1、去程:7:04 --11:32高铁 高铁右转上售票大厅2楼,穿过候车厅下一楼,上大巴车 ¥10/人 **2、到达:**12点多到达寨子,买门票,美团/抖音:¥78人 3、中饭&a…...

Unity | AmplifyShaderEditor插件基础(第七集:平面波动shader)
目录 一、👋🏻前言 二、😈sinx波动的基本原理 三、😈波动起来 1.sinx节点介绍 2.vertexPosition 3.集成Vector3 a.节点Append b.连起来 4.波动起来 a.波动的原理 b.时间节点 c.sinx的处理 四、🌊波动优化…...

ABAP设计模式之---“简单设计原则(Simple Design)”
“Simple Design”(简单设计)是软件开发中的一个重要理念,倡导以最简单的方式实现软件功能,以确保代码清晰易懂、易维护,并在项目需求变化时能够快速适应。 其核心目标是避免复杂和过度设计,遵循“让事情保…...

AI+无人机如何守护濒危物种?YOLOv8实现95%精准识别
【导读】 野生动物监测在理解和保护生态系统中发挥着至关重要的作用。然而,传统的野生动物观察方法往往耗时耗力、成本高昂且范围有限。无人机的出现为野生动物监测提供了有前景的替代方案,能够实现大范围覆盖并远程采集数据。尽管具备这些优势…...

莫兰迪高级灰总结计划简约商务通用PPT模版
莫兰迪高级灰总结计划简约商务通用PPT模版,莫兰迪调色板清新简约工作汇报PPT模版,莫兰迪时尚风极简设计PPT模版,大学生毕业论文答辩PPT模版,莫兰迪配色总结计划简约商务通用PPT模版,莫兰迪商务汇报PPT模版,…...
解决:Android studio 编译后报错\app\src\main\cpp\CMakeLists.txt‘ to exist
现象: android studio报错: [CXX1409] D:\GitLab\xxxxx\app.cxx\Debug\3f3w4y1i\arm64-v8a\android_gradle_build.json : expected buildFiles file ‘D:\GitLab\xxxxx\app\src\main\cpp\CMakeLists.txt’ to exist 解决: 不要动CMakeLists.…...
6个月Python学习计划 Day 16 - 面向对象编程(OOP)基础
第三周 Day 3 🎯 今日目标 理解类(class)和对象(object)的关系学会定义类的属性、方法和构造函数(init)掌握对象的创建与使用初识封装、继承和多态的基本概念(预告) &a…...

[拓扑优化] 1.概述
常见的拓扑优化方法有:均匀化法、变密度法、渐进结构优化法、水平集法、移动可变形组件法等。 常见的数值计算方法有:有限元法、有限差分法、边界元法、离散元法、无网格法、扩展有限元法、等几何分析等。 将上述数值计算方法与拓扑优化方法结合&#…...