跳到主要内容

React 19 弃用 forwardRef:ref 传参方式的前后对比

· 阅读需 5 分钟

在 React 里,「把一个 DOM 节点的引用从父组件传给子组件」——这么简单一件事,React 18 及之前却得包一层 forwardRef

import { forwardRef, useRef } from 'react';

const FancyInput = forwardRef(function FancyInput(props, ref) {
return <input ref={ref} {...props} />;
});

function App() {
const inputRef = useRef<HTMLInputElement>(null);
return <FancyInput ref={inputRef} />;
}

为什么不能直接写 function FancyInput(props) 然后取 props.ref?因为在 React 19 之前,ref 不是普通 prop——它是 React 内部的特殊属性:类组件通过它拿到实例引用,而函数组件默认什么都收不到,只能靠 forwardRef 显式接收再转发。

React 19 把这件事彻底改掉了:ref 变成了普通 prop,forwardRef 被官方标记为 deprecated(弃用,但兼容可用)。新代码不再需要它。

React 19:ref 变成普通 prop

同一个组件的新写法:

import { useRef } from 'react';

function FancyInput({ ref, ...props }: { ref?: React.Ref<HTMLInputElement> }) {
return <input ref={ref} {...props} />;
}

function App() {
const inputRef = useRef<HTMLInputElement>(null);
return <FancyInput ref={inputRef} />;
}

区别就两处:函数签名里直接解构出 ref,外层不再包 forwardRef。对调用方(App)来说,代码一模一样。

前后对比

1. 单层转发:少一层 wrapper

React 18 及之前React 19
接收 ref组件被 forwardRef 包一层,回调多收一个 ref 参数ref 就是 props 里的一个字段,直接解构
转发给原生元素<input ref={ref} {...props} />一样

2. 多层透传:从「每层包 forwardRef」到「直接 spread」

旧写法,三层组件每一层都要包一层:

const A = forwardRef((p, ref) => <B ref={ref} {...p} />);
const B = forwardRef((p, ref) => <C ref={ref} {...p} />);
const C = forwardRef((p, ref) => <input ref={ref} {...p} />);

新写法,ref 既然是普通 prop,直接 spread 就透传了,中间层连解构都不用:

function A(props: ComponentProps<typeof B>) {
return <B {...props} />;
}
function B(props: ComponentProps<typeof C>) {
return <C {...props} />;
}
function C({ ref, ...props }: ComponentProps<'input'>) {
return <input ref={ref} {...props} />;
}

forwardRef 那层包裹、每层组件函数的多余参数,全部消失——透传层甚至不需要知道自己转发了 ref。

3. useImperativeHandle:直接对 ref prop 用

要给父组件暴露命令式方法(比如 focus()),旧写法必须写在 forwardRef 组件里;新写法直接在函数组件里对 ref 用:

// 旧
const MyInput = forwardRef((props, ref) => {
useImperativeHandle(ref, () => ({ focus: () => inputRef.current?.focus() }));
return <input ref={inputRef} {...props} />;
});

// 新
function MyInput({ ref, ...props }: { ref?: React.Ref<HTMLInputElement> }) {
useImperativeHandle(ref, () => ({ focus: () => inputRef.current?.focus() }));
return <input ref={inputRef} {...props} />;
}

useImperativeHandle 本身的行为没变,变的只是「ref 从哪来」。

4. ref cleanup:React 19 的新能力

顺带一提,React 19 的 ref 回调支持返回清理函数——ref 变化或组件卸载时执行,天然解决「手动清理 observer / listener」的脏活:

function VideoPlayer(props: ComponentProps<'video'>) {
return (
<video
ref={(node) => {
if (node) {
const observer = new IntersectionObserver(handleVisible, {});
observer.observe(node);
return () => observer.disconnect(); // ref 变化/卸载时自动清理
}
}}
{...props}
/>
);
}

TypeScript 类型

新写法里 ref 的推荐类型是 React.Ref<T>(可接收对象 ref 和回调 ref,和原生元素行为一致):

function MyInput({ ref }: { ref?: React.Ref<HTMLInputElement> }) { ... }

如果你就是想把所有 props 原样透传给原生元素,直接用 React.ComponentProps<'input'>,它已经包含了 ref

function MyInput(props: React.ComponentProps<'input'>) {
return <input {...props} />;
}

为什么这么改

forwardRef 的问题从来不是「不能用」,而是:

  1. 样板代码:每个要转发 ref 的组件都得包一层,多层嵌套时尤其啰嗦
  2. 组件树里多一层forwardRef 会在渲染树里多加一个组件,影响 DevTools 可读性,也让 memo 失效的排查变难
  3. 心智负担:类组件能通过 ref 拿实例,函数组件却要 forwardRef——这个「函数组件是二等公民」的例外本身就是设计缺陷
  4. ref 无法被读取:因为 ref 不进入 props,库代码连「这个组件有没有 ref」都判断不了

把 ref 变成普通 prop 之后,函数组件和类组件对齐了,ref 的流转规则和其他任何 prop 完全一致——不用再记住「ref 是那个特殊的」。

迁移建议

  • 旧代码不用急着改forwardRef 只是 deprecated,依然可用,未来大版本才可能移除。官方提供了 codemod 可以自动把 forwardRef 批量转成 ref-prop 写法
  • 新代码直接收 ref prop,别再包 forwardRef
  • 配套变化:useRef 现在必须传初始值useRef() 不带参数在 React 19 会直接报错
  • 一个容易踩的坑:Server Components 里 forwardRef 无法转发 ref——ref 天生是客户端概念,需要 ref 的组件要放在 'use client' 边界内自己用 useRef() 创建
  • 想强制团队用新写法,可以用 lint 规则(如 Biome 的 noReactForwardRef)标记残留的 forwardRef

总结

React 19 把 ref 从「特殊保留属性」降级成了「普通 prop」。这看起来只是 API 简化,本质上是消除一个语言设计上的例外:函数组件过去拿个 DOM 引用都得套壳。现在 forwardRef 退场,ref 的接收、透传、命令式句柄、清理逻辑,全都遵循和 props 一样的规则——写起来少一层壳,读起来少一个特例。