深入解析 React 19 中 ref 作为普通 prop 直接传递的新特性,对比 React 18 的 forwardRef 写法差异,提供平滑迁移步骤、TypeScript 类型处理及场景决策指南。
React19的ref可以直接当prop了,forwardRef还写吗?
时间:2026-11-13
浏览:399
文章目录
核心结论先行:在 React 19 中,函数组件已经可以直接将 ref 当作普通 prop 接收,不再需要使用 forwardRef 包裹。React 官方已将 forwardRef 正式标记为弃用(Deprecated),未来主版本将彻底移除。对于全新项目与自用业务组件,坚决不再写 forwardRef;如果是需要同时支持 React 18/17 的开源通用组件库,则现阶段仍需保留 forwardRef 以保证向下兼容。
一、底层机制:为什么以前需要 forwardRef,现在不需要了?
在 React 18 及更早的版本中,JSX 在被编译器(Babel 或 SWC)转换为虚拟 DOM 节点时,key 和 ref 被视为特殊保留关键字。React 的底层运行时会在将属性注入组件函数之前,主动把 ref 从 props 对象中剔除。如果在普通的函数组件上传递了 ref,组件内部在 props.ref 中只能拿到 undefined,同时开发控制台还会抛出一条警告:“Function components cannot be given refs”。
为了解决函数组件向下透传 DOM 引用的问题,React 16.3 引入了 forwardRef。它本质上是一个高阶包装器,要求组件改为接收两个参数 (props, ref),并由 React 运行时把剥离出来的 ref 注入到第二个参数中。这种设计不仅打破了“组件参数只有 props 一个对象”的统一心智模型,还引入了额外的嵌套层级、复杂的泛型参数以及 DevTools 中匿名组件命名(displayName)的问题。
在 React 19 中,React 核心团队彻底重构了 JSX 运行时与属性分发逻辑。ref 不再被特殊剥离,而是被当成与 className、onClick 完全等同的一等公民属性。组件函数回归为纯粹的单一入参形态,直接在解构中读取 ref 即可。
二、写法对照:从 forwardRef 到普通 ref prop
理解机制转变之后,我们来看最常见的输入框(Input)组件在新旧版本中的具体代码形态差异。
1. React 18 旧写法(二参数 + 高阶包裹)
// React 18:必须用 forwardRef 包裹,且显式补充 displayName
import React, { forwardRef } from 'react';
interface InputProps extends React.InputHTMLAttributes<HTMLInputElement> {
label: string;
}
export const CustomInput = forwardRef<HTMLInputElement, InputProps>(
({ label, ...restProps }, ref) => {
return (
<div className="input-group">
<label>{label}</label>
<input ref={ref} {...restProps} />
</div>
);
}
);
CustomInput.displayName = 'CustomInput';
2. React 19 新写法(标准单参数解构)
// React 19:普通的函数组件,ref 直接出现在 props 中
import React from 'react';
interface InputProps extends React.InputHTMLAttributes<HTMLInputElement> {
label: string;
ref?: React.Ref<HTMLInputElement>; // 在 React 19 中直接声明在 props 中
}
export function CustomInput({ label, ref, ...restProps }: InputProps) {
return (
<div className="input-group">
<label>{label}</label>
<input ref={ref} {...restProps} />
</div>
);
}
对比两段代码可以发现,React 19 的书写体验有了质的提升:
- 结构扁平清晰:移除了高阶包裹函数,组件恢复成最基础的
function声明,不需要额外的括号和嵌套。 - 调试名称天然保留:无需为了 React DevTools 正确显示而单独挂载
displayName。 - 参数统一规范:所有输入属性全部归拢于第一个参数对象,支持属性重命名与默认值设定。
三、已有项目平滑迁移的 4 个步骤
当现有项目升级到 React 19 时,不必一次性重写所有组件,但对于正在维护或新建的模块,可以按照以下 4 个步骤有序迁移:
- 移除外层包裹:删除
React.forwardRef(...)包装函数,恢复为普通的具名函数导出export function Component(props)。 - 调整参数列表:删除原本独立的第二参数
ref,在首个入参对象的解构中显式添加ref(例如{ ref, children, ...rest })。 - 更新 TypeScript 类型:删除
forwardRef<RefType, PropsType>的双泛型配置,直接在 Props 接口内声明ref?: React.Ref<TargetElement>,或者直接使用React.ComponentPropsWithRef<'input'>。 - 校验命令式 Handle:如果组件配合了
useImperativeHandle,检查其首个入参,直接传入解构出来的refprop 即可。
四、配合 useImperativeHandle 的新姿势
很多开发者在初见“弃用 forwardRef”时会产生疑虑:如果组件需要向父级暴露命令式方法(例如 focus()、scrollIntoView() 或自定义校验方法),原本依赖 forwardRef 的 useImperativeHandle 还能继续使用吗?
答案是:完全可以,而且写法更为直接。useImperativeHandle 的机制并没有改变,它只需要接收一个有效的 ref 实例作为第一个参数。在 React 19 中,这个 ref 同样直接取自 props:
import React, { useRef, useImperativeHandle } from 'react';
export interface ModalHandle {
open: () => void;
close: () => void;
}
interface ModalProps {
title: string;
ref?: React.Ref<ModalHandle>;
}
export function ModalDialog({ title, ref }: ModalProps) {
const dialogRef = useRef<HTMLDialogElement>(null);
// 直接将来自 props 的 ref 传给 useImperativeHandle
useImperativeHandle(ref, () => ({
open: () => dialogRef.current?.showModal(),
close: () => dialogRef.current?.close(),
}));
return (
<dialog ref={dialogRef} className="app-modal">
<h3>{title}</h3>
<button onClick={() => dialogRef.current?.close()}>关闭</button>
</dialog>
);
}
可以看到,命令式句柄的暴露不再强制捆绑 forwardRef,代码逻辑紧凑自洽。
五、TypeScript 类型生态的改变
在 React 18 中,为 forwardRef 编写正确的 TypeScript 类型常常是团队新人的痛点。由于 forwardRef<T, P> 的泛型顺序是“Ref 类型在前,Props 类型在后”,不仅反直觉,而且在需要同时支持泛型组件(Generic Components)时,往往需要复杂的类型断言或额外写辅助函数。
在 React 19(配合最新版本的 @types/react@^19.0.0)中,这一体验得到了极大改善:
| 对比维度 | React 18 旧写法 | React 19 新特性 |
|---|---|---|
| 组件定义方式 | const C = forwardRef<Ref, Props>((props, ref) => ...) |
function C(props: Props) |
| 泛型组件支持 | 非常复杂,必须借助 as unknown as 类型强转 |
天然支持:function Select<T>(props: SelectProps<T>) |
| 内置 Props 工具类型 | 需区分 ComponentPropsWithoutRef 与 WithRef |
ComponentProps 默认包含 ref,模型彻底统一 |
| Ref 属性解构与重命名 | 不支持重命名(因为 ref 是独立参数) | 完全支持:({ ref: targetRef, ...rest }) |
六、决策指南:什么情况下还必须保留 forwardRef?
虽然 React 19 本身完全支持弃用 forwardRef,但在真实的工程实践中,是否立即全部删除取决于你所维护项目的类型与受众范围:
场景 1:全新业务项目或微前端应用(强推荐:坚决不写)
如果你的应用基线依赖已经明确锁定在 React 19("react": "^19.0.0"),请在新写的每一个组件中直接使用 ref prop。不必保留任何 forwardRef,这是 React 团队明确给出的未来演进路线。
场景 2:从 React 18 升级的现有业务系统(策略:暂不急改,按需重构)
React 19 保证了严格的向后兼容性。现存代码中的 forwardRef 在运行时仍然能正常执行,不会中断业务或造成构建错误。React 仅会在开发模式的控制台中打印弃用提示。建议制定渐进重构策略:在重构旧模块或修复缺陷时顺手改写,或者使用官方提供的 codemod 批量自动化转换。
场景 3:面向社区发布的通用 UI 组件库 / SDK(必须保留:向后兼容)
如果你的组件库发布的 npm 包需要被 React 18 甚至 React 17 的宿主项目安装使用(例如 peerDependencies: ">=18.0.0"),则现阶段必须继续保留 forwardRef。因为 React 18 的运行时无法识别作为普通 prop 传递的 ref,一旦宿主在 React 18 下直接传递 ref,组件将无法接收到底层 DOM 引用,引发功能异常。
若组件库计划完全弃用 forwardRef,请务必作为破坏性变更(Breaking Change)发布新的主版本(Major Version),并在 peerDependencies 中明确将 react 的最低要求提升至 >=19.0.0。
七、常见疑问与避坑事项
Q1:如果不修改旧组件中的 forwardRef,React 19 会报错崩溃吗?
不会。React.forwardRef 目前只是被标记为 deprecated,其功能在 React 19 的整个生命周期内依然保持稳定。React 团队通常会留出至少一个完整的大版本缓冲周期,因此存量项目可以平稳过渡。
Q2:ref 作为普通 prop 后,是否可以传递给子组件再次深层透传?
完全可以。在 React 18 中,如果想将 ref 从 A 穿透给 B 再传给 input,往往需要层层包裹 forwardRef;而在 React 19 中,由于 ref 就是普通的 JavaScript 变量,你可以像传递任何回调函数一样自由传递或重命名:
// 自由转发或解构重命名
function OuterWrapper({ ref, ...props }) {
return <InnerSection targetRef={ref} {...props} />;
}
Q3:类组件(Class Components)支持将 ref 当作普通 prop 吗?
不支持。类组件的 ref 属性在语法上始终绑定为该类组件的实例对象(Component Instance)。因此,直接将 ref 作为 prop 解构的特性仅适用于函数组件(Function Components)。
八、全文总结与升级检查清单
React 19 将 ref 归为普通 prop 是一项极具工程价值的瘦身改动,消除了长期以来函数组件为了处理 ref 而不得不引入的复杂封装。以下是升级决策的核心备忘卡片:
- 新写组件:一律使用
function MyComp({ ref, ...props }),不再导入forwardRef。 - TypeScript:在 Props 接口中添加
ref?: React.Ref<T>,摆脱双泛型包裹。 - 命令式方法:直接调用
useImperativeHandle(ref, () => ({ ... }))。 - 组件库维护:需兼容 React 18 则保留
forwardRef;纯 React 19 库果断废弃。
实用工具推荐
绿化、压缩、噪点和虚拟水印,自由调节做旧程度。
选择画质和输出格式,预览压缩前后的大小。