forwardRef 让父组件可以通过你的组件,把一个 ref 传给组件内部的 DOM 元素,这样父组件就能对该元素调用 focus()、scrollIntoView() 等方法。在 React 19 中你已经不再需要它:函数组件会把 ref 当作普通 prop 接收。forwardRef 仍然可用,而且大多数现有代码里都能看到它,所以本页两种写法都会展示。
forwardRef 接收一个带两个参数的渲染函数:props,以及父组件传入的 ref。ref 不在 props 里面,而是单独传进来。点击 Focus,控制台会确认 inputRef.current 就是真实的 <input> 节点。
React 19 的写法:ref 就是一个 prop
从 React 19 开始,函数组件会像接收其他 prop 一样,在 props 中拿到 ref。不需要包装,也没有第二个参数:
输入一些文字,然后点击 Log value:父组件直接从 DOM 节点读取输入框当前的文本。把 <input> 上的 ref={ref} 删掉,再点击 Focus:控制台会显示一个错误,因为 inputRef.current 现在是 null。
forwardRef(所有版本) | ref prop(React 19+) | |
|---|---|---|
| 组件如何拿到 ref | 第二个参数 (props, ref) | props.ref |
| 是否需要包装 | 是 | 否 |
能否配合 useImperativeHandle | 能 | 能 |
| 现状 | 可用,计划弃用 | 编写新代码的方式 |
子组件忽略的 ref 始终是 null
把 ref 传给组件,本身什么都不会发生。组件必须把它放到某个元素上。否则,ref.current 会一直是 null:
控制台显示 Forgetful ref: null 和 Careful ref: INPUT。这是自定义组件上出现“ref.current is null”最常见的原因。在 React 19 之前,除非用 forwardRef 包装,Careful 也会是 null,因为 ref 从来不会交给这个函数。
useImperativeHandle:暴露更小的 API
把整个 DOM 节点交给父组件,意味着父组件可以对它做任何事:修改样式、删除子元素、读取你本想保密的值。useImperativeHandle 让你自己决定 ref.current 里包含什么:
最后一个按钮打印 ["focus", "clear"]:父组件只拿到这两个方法,别无其他,也拿不到 input 节点。给这个对象加一个 select() 方法,再加一个调用它的按钮,父组件就恰好多了一项能力。
子组件为真实的输入框保留了自己的 ref,即 inputRef,而 handle 把它包装了起来。第三个参数是依赖数组,和 useEffect 的一样:这些值变化时,handle 会重新构建。
谨慎使用这个 Hook。父组件想做的大多数事情(打开、关闭、显示错误)更适合用 props 实现,比如 isOpen 或 error。命令式方法适用于那些没有对应 prop 的操作:聚焦、滚动、播放视频、启动动画。
子组件自己也使用这个 ref
有时子组件自己也需要同一个 DOM 节点来做事,比如测量它的尺寸,或者在出错后聚焦它,而父组件同时也持有它的 ref。一个 ref 属性只能接收一个值,所以要用 ref 回调把两者合并:
function AutoGrowTextarea({ ref, ...props }) {
const localRef = useRef(null);
function setRefs(node) {
localRef.current = node;
if (typeof ref === 'function') ref(node);
else if (ref) ref.current = node;
}
return <textarea ref={setRefs} {...props} />;
}
父组件的 ref 可能是 useRef 返回的对象,也可能是一个函数,所以两种情况都要处理。如果父组件只需要几个操作,上面的 useImperativeHandle 是更清爽的选择,因为子组件可以把节点留给自己。
跨越多层传递 ref
ref 一次只能传过一个组件。如果 Form 渲染 Field,Field 渲染 TextInput,TextInput 再渲染 <input>,那么每个组件都必须把 ref 传给下一个。在 React 19 中,这只是多转发一个 prop(<TextInput ref={ref} />);用 forwardRef 时,每一层都得包装。在旧版本中,展开 props({...props})不会带上它,因为 React 19 之前,ref 从来不是 props 的一部分。
设计系统组件通常是这一点最重要的地方:包装了原生元素的 Button、Input 或 Select 应该转发它的 ref,这样使用它的应用才能聚焦它、测量它,或者把它交给负责定位弹出层的库。
从 forwardRef 迁移
改动是机械性的:去掉包装,从 props 中读取 ref。
// Before
const Button = forwardRef(function Button({ variant, ...props }, ref) {
return <button ref={ref} className={variant} {...props} />;
});
// After (React 19)
function Button({ variant, ref, ...props }) {
return <button ref={ref} className={variant} {...props} />;
}
不用着急。forwardRef 在 React 19 中仍然可用,而必须支持 React 18 的库也得保留它,因为 React 18 不会把 ref 作为 prop 传递。类组件不受影响:类组件上的 ref 仍然指向组件实例。
TypeScript
使用 forwardRef 时,类型参数的顺序是先 ref 类型,再 props:
import { forwardRef } from 'react';
type FancyInputProps = { label: string };
const FancyInput = forwardRef<HTMLInputElement, FancyInputProps>(
function FancyInput({ label }, ref) {
return <input ref={ref} aria-label={label} />;
}
);
在 React 19 中,像其他 prop 一样给 ref 标注类型即可。ComponentProps<'input'> 已经包含了它:
import { useImperativeHandle, useRef, type ComponentProps, type Ref } from 'react';
function FancyInput(props: ComponentProps<'input'>) {
return <input {...props} />;
}
type SearchHandle = { focus: () => void; clear: () => void };
function SearchBox({ ref }: { ref?: Ref<SearchHandle> }) {
const inputRef = useRef<HTMLInputElement>(null);
useImperativeHandle(ref, () => ({
focus: () => inputRef.current?.focus(),
clear: () => {
if (inputRef.current) inputRef.current.value = '';
},
}));
return <input ref={inputRef} />;
}
// In the parent
const searchRef = useRef<SearchHandle>(null);
useRef 一页讲解了 ref 本身:访问 DOM、在不触发渲染的情况下保留值,以及 ref 回调。关于给组件标注类型的一般方法,请参阅 React 与 TypeScript。
常见问题
React 中的 forwardRef 有什么作用?
它包装一个函数组件,让父组件传入的 ref 作为第二个参数 (props, ref) 到达组件。组件再把这个 ref 放到某个 DOM 节点上,父组件就能对它调用 focus() 之类的方法。
forwardRef 在 React 19 中被弃用了吗?
还没有,它仍然可以使用。React 19 会把 ref 作为普通 prop 传给函数组件,所以新代码不需要它,React 团队也表示计划在未来的版本中弃用 forwardRef。
为什么自定义组件上的 ref 是 null?
组件收到了 ref,却没有把它放到任何元素上。把它挂到内部的 DOM 节点上:<input ref={ref} />。在 React 19 之前,除非组件用 forwardRef 包装,否则 ref 根本不会传进来。
useImperativeHandle 是做什么用的?
它替换父组件在 ref.current 中看到的内容。你返回的不再是整个 DOM 节点,而是一个只包含你所选方法的对象,比如 focus 和 clear。
在 React 19 中,类组件也会把 ref 当作 prop 接收吗?
不会。类组件上的 ref 仍然指向组件实例。这项改动只适用于函数组件。