让你 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...
浅谈 React Hooks
React Hooks 是 React 16.8 引入的一组 API,用于在函数组件中使用 state 和其他 React 特性(例如生命周期方法、context 等)。Hooks 通过简洁的函数接口,解决了状态与 UI 的高度解耦,通过函数式编程范式实现更灵活 Rea…...
深度学习在微纳光子学中的应用
深度学习在微纳光子学中的主要应用方向 深度学习与微纳光子学的结合主要集中在以下几个方向: 逆向设计 通过神经网络快速预测微纳结构的光学响应,替代传统耗时的数值模拟方法。例如设计超表面、光子晶体等结构。 特征提取与优化 从复杂的光学数据中自…...
反向工程与模型迁移:打造未来商品详情API的可持续创新体系
在电商行业蓬勃发展的当下,商品详情API作为连接电商平台与开发者、商家及用户的关键纽带,其重要性日益凸显。传统商品详情API主要聚焦于商品基本信息(如名称、价格、库存等)的获取与展示,已难以满足市场对个性化、智能…...
k8s从入门到放弃之Ingress七层负载
k8s从入门到放弃之Ingress七层负载 在Kubernetes(简称K8s)中,Ingress是一个API对象,它允许你定义如何从集群外部访问集群内部的服务。Ingress可以提供负载均衡、SSL终结和基于名称的虚拟主机等功能。通过Ingress,你可…...
蓝牙 BLE 扫描面试题大全(2):进阶面试题与实战演练
前文覆盖了 BLE 扫描的基础概念与经典问题蓝牙 BLE 扫描面试题大全(1):从基础到实战的深度解析-CSDN博客,但实际面试中,企业更关注候选人对复杂场景的应对能力(如多设备并发扫描、低功耗与高发现率的平衡)和前沿技术的…...
MySQL账号权限管理指南:安全创建账户与精细授权技巧
在MySQL数据库管理中,合理创建用户账号并分配精确权限是保障数据安全的核心环节。直接使用root账号进行所有操作不仅危险且难以审计操作行为。今天我们来全面解析MySQL账号创建与权限分配的专业方法。 一、为何需要创建独立账号? 最小权限原则…...
算法岗面试经验分享-大模型篇
文章目录 A 基础语言模型A.1 TransformerA.2 Bert B 大语言模型结构B.1 GPTB.2 LLamaB.3 ChatGLMB.4 Qwen C 大语言模型微调C.1 Fine-tuningC.2 Adapter-tuningC.3 Prefix-tuningC.4 P-tuningC.5 LoRA A 基础语言模型 A.1 Transformer (1)资源 论文&a…...
Mysql中select查询语句的执行过程
目录 1、介绍 1.1、组件介绍 1.2、Sql执行顺序 2、执行流程 2.1. 连接与认证 2.2. 查询缓存 2.3. 语法解析(Parser) 2.4、执行sql 1. 预处理(Preprocessor) 2. 查询优化器(Optimizer) 3. 执行器…...
三分算法与DeepSeek辅助证明是单峰函数
前置 单峰函数有唯一的最大值,最大值左侧的数值严格单调递增,最大值右侧的数值严格单调递减。 单谷函数有唯一的最小值,最小值左侧的数值严格单调递减,最小值右侧的数值严格单调递增。 三分的本质 三分和二分一样都是通过不断缩…...
解读《网络安全法》最新修订,把握网络安全新趋势
《网络安全法》自2017年施行以来,在维护网络空间安全方面发挥了重要作用。但随着网络环境的日益复杂,网络攻击、数据泄露等事件频发,现行法律已难以完全适应新的风险挑战。 2025年3月28日,国家网信办会同相关部门起草了《网络安全…...
