React19为什么ref可以直接当prop用?forwardRef还用吗?
深入解析 React 19 中为什么 ref 可以直接作为普通 prop 传递给函数组件,剖析其底层原理变化、forwardRef 的废弃周期、具体迁移步骤以及常见踩坑点。
ref 的特殊提取行为,让 ref 成为与普通属性(如 onClick、className)完全一致的标准 prop。因此,函数组件无需再使用 forwardRef 包装,直接在参数 props 中解构 ref 即可使用。forwardRef 在 React 19 中已被正式标记为废弃(Deprecated);新建组件应当坚决不再使用它,而已有的 forwardRef 在 React 19 仍可正常运行,未来版本才会彻底移除。
一、为什么过去 ref 不能直接当 prop 传递?
要理解 React 19 的突破,首先需要了解为什么过去的 React 那么“排斥”给函数组件直接传 ref。
在 React 19 之前的漫长版本里,ref 和 key 并不被视为常规的 props,而是 React 的内部保留字(Reserved Props)。无论你写的是类组件还是函数组件,JSX 编译器(如 React.createElement 或旧版 jsx-runtime)在构建虚拟 DOM 元素对象(React Element)时,都会运行一段拦截剔除逻辑:强制把传入对象中的 ref 和 key 抽离出来,挂在虚拟 DOM 顶层作为元数据(即 element.ref 和 element.key),而剩余的属性才会装进 element.props。
ref.current 上。
然而随着 React 16.8 全面迈入函数组件时代,函数组件本身没有实例。当开发者尝试对一个自定义函数组件写下 <MyInput ref={inputRef} /> 时,React 遇到了根本性的冲突:
- 无法挂载实例:函数组件每次渲染只是执行一次纯函数调用,根本不存在组件实例供 React 自动挂载。
- 组件内部读不到:由于
ref早在 JSX 解析阶段就被从props中剔除并塞入element.ref,函数组件内部的props.ref永远是undefined。
为了防止开发者误以为自己能像类组件一样给函数组件挂 ref,React 内部加入了硬性防御机制:一旦检测到目标是普通函数组件却收到了 ref,直接在控制台抛出警告:Warning: Function components cannot be given refs. Did you mean to use React.forwardRef()?。
为了解决“把父级 ref 透传给函数组件内部底层 DOM 节点”的需求,React 16.3 推出了 React.forwardRef 高阶函数作为妥协产物:它要求开发者必须多包一层包装器,React 调和引擎在遇到 forwardRef 类型的组件时,再通过特权逻辑把剥离出来的 element.ref 作为第二参数传入组件函数:(props, ref) => JSX。这种“补丁”设计长期以来打破了函数组件“唯一入参即 props”的优雅契约。
二、React 19 底层发生了什么变化?
React 19 带来了全新的 JSX 编译与元素调度模型,彻底终结了这种割裂的底层机制:
- JSX 转换器一视同仁:在 React 19 的新 JSX 转换逻辑中,
ref不再被特殊抽离。当你在函数组件上声明<MyInput ref={myRef} />时,ref完整地保留在props对象中,即直接作为props.ref传递。 - 调和引擎直接派发:函数组件被调用时,接收到的唯一参数
props中已经天然包含了ref。组件内部可以直接读取它,并按照普通变量透传给原生 DOM(例如<input ref={ref} />),React 负责在 Commit 阶段将原生 DOM 节点绑定至该引用。 - 消除特例分支:React 核心不再需要维护专门针对函数组件 ref 的警告拦截,也不再需要对
forwardRef进行双参数特殊调用解包。
三、forwardRef 还用吗?各类场景的明确判断
很多开发者最关心的是:旧项目里的 forwardRef 会报错吗?新项目到底还写不写?答案分三种明确场景:
| 项目场景 | 是否使用 forwardRef | 处理策略与官方建议 |
|---|---|---|
| 全新 React 19 业务项目 | 坚决不用 | 直接以普通 prop 解构方式编写组件,享受最简语法与清晰类型。 |
| 刚升级到 React 19 的旧业务工程 | 无需急躁,逐步替换 | React 19 完全向下兼容已有 forwardRef,不会引发崩溃;可在常规重构时通过官方 Codemod 自动化批量替换。 |
| 开源/公共 NPM 组件库 | 视依赖版本而定 | 若 peerDependencies 仅支持 >= 19.0.0,全面移除;若需兼容 React 18 及更早版本的用户,必须保留。 |
forwardRef 依然能正常运行,但它已被标注为 @deprecated。React 官方已明确表示,在未来的 React 大版本(如 React 20)中,forwardRef 将被彻底从包中移除。
四、代码实战:从 forwardRef 切换到 React 19 原生写法
我们通过两个实际开发中最常用的场景,直观对比写法的变化。
1. 常见 DOM 引用转发组件
在 React 18 中,封装一个带 Label 的自定义 Input 必须通过 forwardRef 高阶函数包裹:
// ❌ React 18 旧写法:需要高阶包裹、反向泛型与 displayName 声明
import React, { forwardRef } from 'react';
interface TextInputProps extends React.InputHTMLAttributes<HTMLInputElement> {
label: string;
}
const TextInput = forwardRef<HTMLInputElement, TextInputProps>((props, ref) => {
const { label, ...rest } = props;
return (
<label className="field">
<span>{label}</span>
<input ref={ref} {...rest} />
</label>
);
});
TextInput.displayName = 'TextInput'; // 避免 DevTools 中显示为 Anonymous
在 React 19 中,代码恢复为最纯粹的标准函数声明:
// ✔ React 19 新写法:回归普通函数与单一 props 入参
import React from 'react';
interface TextInputProps extends React.ComponentProps<'input'> {
label: string;
// ref 可以直接在接口中显式声明(也可通过 ComponentProps 继承获得)
ref?: React.Ref<HTMLInputElement>;
}
export function TextInput({ ref, label, ...props }: TextInputProps) {
return (
<label className="field">
<span>{label}</span>
<input ref={ref} {...props} />
</label>
);
}
2. 配合 useImperativeHandle 暴露方法
许多开发者会有疑问:如果组件不是转发真实 DOM,而是使用 useImperativeHandle 向父级暴露自定义 API 实例,在没有 forwardRef 的情况下还能工作吗?
完全可以! useImperativeHandle 的第一个参数本质上就是一个 ref 对象或回执函数。在 React 19 中,直接把从 props 里解构出来的 ref 传进去即可,完全脱离 forwardRef:
// ✔ React 19 中无需 forwardRef 的 useImperativeHandle 写法
import React, { useRef, useImperativeHandle } from 'react';
export interface ModalHandle {
open: () => void;
close: () => void;
}
interface ModalProps {
ref?: React.Ref<ModalHandle>;
title: string;
}
export function Modal({ ref, title }: ModalProps) {
const dialogRef = useRef<HTMLDialogElement>(null);
// 直接将 props 中拿到的 ref 传入首个参数
useImperativeHandle(ref, () => ({
open() {
dialogRef.current?.showModal();
},
close() {
dialogRef.current?.close();
}
}));
return (
<dialog ref={dialogRef}>
<h3>{title}</h3>
</dialog>
);
}
五、四步标准化重构流程与官方 Codemod
如果你的工程准备全面升级到 React 19 并清除旧的 forwardRef,可以按照以下 4 个标准化步骤落地:
- 全局检索定位:使用 IDE 全局搜索
forwardRef<或React.forwardRef,统计项目中需要重构的组件清单。 - 解开高阶包装:移除外层的
forwardRef(...)调用,将匿名箭头函数还原为规范的具名函数(如function MyComponent),同时删除多余的MyComponent.displayName补丁代码。 - 收拢参数为单对象:把原先第二参数位置的
ref剪切并挪至首个形参的解构对象中,写为{ ref, ...props }。 - 调整 TypeScript 接口并验证:检查 Props 接口,确保继承了
React.ComponentPropsWithRef<'...'>或显式声明了ref?: React.Ref<TargetElement>。
npx codemod@latest react/19/remove-forward-ref
六、必须留意的 3 个边界细节与踩坑点
虽然语法变得更加简洁,但在将 ref 作为标准 prop 使用的过程中,有 3 个细节需要特别留意,避免产生难以觉察的隐患:
1. 剩余参数解构(...rest)会自动捕获 ref
在 React 18 中,即使你使用 const { label, ...rest } = props;,rest 字典里也绝对不会出现 ref。但在 React 19 中,ref 已经变成了普通的 prop。如果你没有在形参中解构出 ref,那么 ...rest 中就会包含 ref!
如果你将这个 rest 原封不动地透传给另外一个自定义组件,而该组件恰好并未设计接收 ref,可能会导致不必要的属性向下穿透或触发非预期的逻辑。防范建议:在包装中间层组件时,若不打算转发 ref,请显式将 ref 剔除或显式具名解构。
2. 渲染期间仍然严禁读取或修改 ref.current
ref 变成普通 prop 仅仅改变了“如何将引用句柄传递进组件”,完全没有改变 React 中 ref 本身的生命周期与纯函数规则。组件的渲染阶段必须保持纯净,绝不能在组件函数体直接读取或修改 ref.current,任何与 DOM 相关的操作仍然应当限定在 useEffect 或用户事件回调函数中。
3. 原生 HTML 标签与 Class 组件行为保持不变
本次语法简化专门针对函数组件(Function Components)。给原生 DOM 元素(如 <div ref={divRef} />)传 ref 依然是获取真实 DOM 节点;给遗留的类组件传 ref 依然是获取该类的实例对象,其底层规则并未发生改动。
七、升级总结与技术决策全景
React 19 将 ref 转为普通 prop 是自 React 16.8 引入 Hooks 以来对函数组件 API 进行的最重要的一次“归一化”梳理。它彻底消除了因为历史遗留包袱而引入的 forwardRef 特例,让函数组件重回“接受一个 props 对象,返回 JSX”的标准模型,大幅降低了初学者的理解成本和 TypeScript 的泛型黑魔法。
简而言之:在任何纯粹的 React 19 环境下,forwardRef 已经没有继续新写的理由。放心大胆地在 props 中解构 ref,享受更清爽直观的代码吧!
ReactCompiler怎么开启?能消除哪些重复渲染?
2026-11-13 08:01:21
学React之前JavaScript要掌握到什么程度?
2026-11-12 23:04:03
React报Eachchildinalistshouldhaveauniquekey怎么解决?
2026-11-12 21:37:29
onClick={handle()}为什么一渲染就执行?
2026-11-12 19:20:59
JSX列表渲染为什么必须加key?key能不能用index?
2026-11-12 14:57:28








