最近强度大,主要是因为在老项目中加新功能,而我作为最底层的coder,毫无话语权,这个业务还是一个需要跨国沟通(任人宰割),要求我们只能加,而且他们code review还要以他们为准,每次耗时,耗心更无语。但对于早已有自我认知的底层人来说,只要给我时间,不压力我就行,同时学习汲取养分,因此也遇到了一些小技巧。
在 React 中,forwardRef 是一个用于将 ref 从父组件"转发"到子组件内部 DOM 节点或类组件实例的高级 API。
为什么需要 forwardRef?
默认情况下,React 的 ref 属性不会像普通 props 那样传递给函数组件。如果你直接在函数组件上绑定 ref,会收到警告且无法获取内部 DOM。forwardRef 就是为了解决这个问题而生的。
⚠️ 注意 :如果你使用的是 React 19+ ,
ref已经可以作为普通 prop 直接传递,不再强制需要forwardRef。但考虑到大量存量项目和 React 18 及以下版本,掌握它仍然非常重要。
核心语法
javascript
const ChildComponent = React.forwardRef((props, ref) => {
// props: 父组件传入的普通属性
// ref: 父组件通过 ref=xxx 传入的引用
return <div ref={ref}>内容</div>;
});
完整实战示例
下面用一个 "可聚焦输入框" 的场景来详细讲解:
1. 子组件:FancyInput(使用 forwardRef)
javascript
import React, { forwardRef } from 'react';
// ✅ 使用 forwardRef 包裹函数组件
const FancyInput = forwardRef(({ label, placeholder, ...restProps }, ref) => {
return (
<div className="fancy-input-wrapper">
<label>{label}</label>
{/* ✅ 关键:将收到的 ref 绑定到真正的 DOM input 元素上 */}
<input
ref={ref}
placeholder={placeholder}
className="fancy-input"
{...restProps}
/>
</div>
);
});
// 可选:设置 displayName,方便 DevTools 调试
FancyInput.displayName = 'FancyInput';
export default FancyInput;
2. 父组件:使用 ref 控制子组件
ini
import React, { useRef } from 'react';
import FancyInput from './FancyInput';
function ParentForm() {
const inputRef = useRef(null);
const handleFocus = () => {
// ✅ 可以直接访问子组件内部的真实 DOM 节点
inputRef.current?.focus();
};
const handleClear = () => {
if (inputRef.current) {
inputRef.current.value = '';
inputRef.current.focus();
}
};
return (
<div>
{/* ✅ ref 像普通属性一样传给被 forwardRef 包裹的组件 */}
<FancyInput
ref={inputRef}
label="用户名"
placeholder="请输入用户名"
/>
<button onClick={handleFocus}>聚焦输入框</button>
<button onClick={handleClear}>清空并聚焦</button>
</div>
);
}
进阶用法:暴露自定义方法(useImperativeHandle)
有时你不想暴露整个 DOM,而是只想暴露特定的方法给父组件:
javascript
import React, { forwardRef, useRef, useImperativeHandle } from 'react';
const CustomSelect = forwardRef(({ options }, ref) => {
const selectRef = useRef(null);
// ✅ 自定义暴露给父组件的实例值
useImperativeHandle(ref, () => ({
// 只暴露 getValue 和 reset 两个方法
getValue: () => selectRef.current?.value,
reset: () => {
if (selectRef.current) selectRef.current.selectedIndex = 0;
},
// 不暴露 focus、blur 等原生 DOM 方法
}));
return (
<select ref={selectRef}>
{options.map((opt) => (
<option key={opt.value} value={opt.value}>
{opt.label}
</option>
))}
</select>
);
});
// 父组件调用
// const selectRef = useRef();
// selectRef.current.getValue() ✅
// selectRef.current.focus() ❌ undefined(被隐藏了)
关键注意事项
| 要点 | 说明 |
|---|---|
| 仅用于函数组件 | 类组件天然支持 ref,无需 forwardRef |
| 必须绑定到 DOM/类组件 | ref 最终要挂在 <div>、<input> 或类组件上,不能挂在另一个函数组件上(除非那个也用了 forwardRef) |
| displayName | 务必设置,否则 DevTools 中显示为 "ForwardRef",难以调试 |
| 不要滥用 | 优先用 props/callback/state 通信,ref 仅用于命令式操作(聚焦、滚动、测量尺寸、集成第三方库) |
| React 19 变化 | React 19 起 ref 可作为普通 prop,forwardRef 逐步废弃 |
何时该用 forwardRef?
- ✅ 封装 UI 库组件(Button、Input、Modal 等),让使用者能获取 DOM
- ✅ 集成第三方非 React 库(如 ECharts、地图SDK)
- ✅ 实现自动聚焦、滚动到视图、动画触发等命令式操作
- ❌ 仅仅为了读取子组件的 state → 应该用 props + 状态提升
- ❌ 触发子组件重新渲染 → 应该用 props 变化驱动
总结:forwardRef 是 React 组件封装体系中打通"声明式"与"命令式"的桥梁,在构建可复用组件库时几乎是必备技能。
✅React 19+(ref 作为普通 prop)✅
javascript
// ref 直接从 props 中解构,无需任何包装
function Input({ ref, ...props }) {
return <input ref={ref} {...props} />;
}
💡 核心变化 :
ref不再是一个特殊的、被 React 内部拦截的属性,它和其他 props(如className、onClick)完全平等。
2. 完整实战示例(React 19+)
子组件:FancyInput
javascript
// React 19: 直接接收 ref 作为 prop
function FancyInput({ label, placeholder, ref, ...restProps }) {
return (
<div className="fancy-input-wrapper">
<label>{label}</label>
{/* ref 直接绑定到 DOM 节点 */}
<input
ref={ref}
placeholder={placeholder}
className="fancy-input"
{...restProps}
/>
</div>
);
}
export default FancyInput;
父组件:使用方式完全不变
javascript
import { useRef } from 'react';
import FancyInput from './FancyInput';
function ParentForm() {
const inputRef = useRef(null);
const handleFocus = () => {
inputRef.current?.focus();
};
return (
<div>
{/* ✅ 调用方式和之前一模一样,对使用者零感知 */}
<FancyInput
ref={inputRef}
label="用户名"
placeholder="请输入用户名"
/>
<button onClick={handleFocus}>聚焦</button>
</div>
);
}
3. useImperativeHandle 同样简化
useImperativeHandle 仍然保留,但配合新的 ref-as-prop 模式更自然:
javascript
import { useRef, useImperativeHandle } from 'react';
// React 19: ref 从 props 获取
function CustomSelect({ options, ref }) {
const selectRef = useRef(null);
// ✅ ref 参数直接使用从 props 传入的 ref
useImperativeHandle(ref, () => ({
getValue: () => selectRef.current?.value,
reset: () => {
if (selectRef.current) selectRef.current.selectedIndex = 0;
},
}));
return (
<select ref={selectRef}>
{options.map((opt) => (
<option key={opt.value} value={opt.value}>{opt.label}</option>
))}
</select>
);
}
4. ⚠️ 迁移注意事项
| 注意点 | 说明 |
|---|---|
| 向后兼容 | React 19 仍然支持 forwardRef,旧代码无需立即修改 |
| TypeScript 类型 | 不再需要 ForwardRefRenderFunction 类型,直接用普通组件 Props 接口 + { ref?: Ref<HTMLInputElement> } 即可 |
| displayName | 不再需要手动设置 displayName,因为组件就是普通函数,DevTools 直接显示函数名 |
| 高阶组件(HOC) | 如果你的 HOC 之前用 forwardRef 包裹来透传 ref,现在可以直接把 ref 当作 prop 透传,大幅简化 HOC 实现 |
| 最低版本要求 | 必须升级到 react@^19.0.0 和 react-dom@^19.0.0 |
TypeScript 示例(React 19+)
typescript
import { type Ref } from 'react';
interface FancyInputProps {
label: string;
placeholder?: string;
ref?: Ref<HTMLInputElement>; // ✅ 直接在 props 接口中声明
}
function FancyInput({ label, placeholder, ref }: FancyInputProps) {
return (
<div>
<label>{label}</label>
<input ref={ref} placeholder={placeholder} />
</div>
);
}
📌 总结
React 19 将 ref "去特殊化",使其回归为普通 prop。这意味着:
- 新代码 :直接使用
ref作为 prop,忘掉forwardRef - 旧代码:继续正常工作,可在日常重构中逐步替换
- 心智模型:不再有"哪些 prop 是特殊的"这种认知负担,所有属性一视同仁