React 中 forwardRef 传递 ref 给子组件
什么是 forwardRef
在 React 中,ref 通常用于获取 DOM 元素或类组件实例的引用。然而,默认情况下无法直接将 ref 通过属性传递给函数子组件,因为 React 不会将 ref 作为普通的 prop 向下传递。forwardRef 正是为解决这一问题而生的高阶函数,它允许函数组件接收并转发 ref 给其内部的某个 DOM 元素或子组件。
简单来说,forwardRef 是 React 提供的一种透传 ref 的机制,让父组件能够直接访问子组件内部的节点或实例。
为什么需要 forwardRef
普通函数组件无法接收 ref
如果你直接给一个函数组件传递 ref,React 会在控制台报错,并且 ref.current 为 null。
function MyInput(props) {
return <input {...props} />;
}
function Parent() {
const inputRef = useRef(null);
return <MyInput ref={inputRef} />; // ⚠️ 警告:函数组件不能接收ref
}
面对这种场景,要么将组件改为 class 组件,要么使用 forwardRef 来显式转发 ref。
封装可复用组件时的刚性需求
很多 UI 组件(如按钮、输入框、自定义弹窗)内部包含原生 DOM 节点。为了使使用者能够通过 ref 聚焦、测量尺寸或直接操作 DOM,就必须使用 forwardRef 将内部元素暴露出去。
forwardRef 的基本语法
React.forwardRef 接受一个渲染函数,该函数接收 props 和 ref 两个参数,并返回 React 元素。
const FancyButton = React.forwardRef((props, ref) => (
<button ref={ref} className="fancy-button">
{props.children}
</button>
));
现在父组件可以将 ref 传递给 FancyButton,并最终附加到内部的 <button> 上。
function App() {
const buttonRef = useRef(null);
useEffect(() => {
console.log(buttonRef.current); // 直接访问button DOM节点
}, []);
return <FancyButton ref={buttonRef}>点击</FancyButton>;
}
实际应用场景
1. 聚焦输入框
最常见的场景是在包裹 input 的组件中暴露 ref,让父组件可以随时调用 focus()。
const TextInput = React.forwardRef((props, ref) => (
<input type="text" ref={ref} {...props} />
));
function Form() {
const inputRef = useRef();
return (
<>
<TextInput ref={inputRef} placeholder="输入内容..." />
<button onClick={() => inputRef.current.focus()}>聚焦输入框</button>
</>
);
}
2. 在 HOC 中传递 ref
当使用高阶组件包装函数组件时,外层组件会“吞噬”掉 ref。此时需要用 forwardRef 将 ref 转发到被包裹的组件。
function withLogging(WrappedComponent) {
return React.forwardRef((props, ref) => {
console.log('组件渲染:', WrappedComponent.name);
return <WrappedComponent {...props} ref={ref} />;
});
}
3. 测量子组件尺寸或位置
将 ref 转发给需要测量的 DOM 元素,配合 useRef 和 ResizeObserver 或 getBoundingClientRect() 实现动态布局。
与 useImperativeHandle 结合使用
有时我们并不希望将整个 DOM 节点暴露给父组件,而是希望提供一个受限的、自定义的命令式接口。这时可以结合 forwardRef 和 useImperativeHandle 实现。
const CustomInput = React.forwardRef((props, ref) => {
const inputRef = useRef(null);
useImperativeHandle(ref, () => ({
focus: () => {
inputRef.current.focus();
},
clear: () => {
inputRef.current.value = '';
},
}));
return <input ref={inputRef} {...props} />;
});
父组件使用时只能访问 focus 和 clear 方法,无法直接操作 DOM 的其他属性,保证了封装性和安全性。
function Parent() {
const inputRef = useRef();
return (
<>
<CustomInput ref={inputRef} />
<button onClick={() => inputRef.current.focus()}>聚焦</button>
<button onClick={() => inputRef.current.clear()}>清空</button>
</>
);
}
在 TypeScript 中使用 forwardRef
在 TypeScript 项目中,需要明确指定 ref 的类型以及组件的 props 类型。
interface Props {
label: string;
}
const MyInput = React.forwardRef<HTMLInputElement, Props>(({ label }, ref) => (
<div>
<span>{label}</span>
<input ref={ref} />
</div>
));
- 第一个泛型参数是
ref所指向的元素类型(HTMLInputElement)。 - 第二个泛型参数是组件的
props类型。
常见错误与注意事项
1. 忘记在渲染函数中附加 ref
forwardRef 接收的回调必须显式把 ref 绑定到内部元素,否则 ref 仍然为 null。
// ❌ 错误:没有使用ref
const Comp = React.forwardRef((props, ref) => <div>...</div>);
// ✅ 正确
const Comp = React.forwardRef((props, ref) => <div ref={ref}>...</div>);
2. 多处转发 ref
一个组件只能转发一个 ref。如果需要多个内部元素被外部访问,应使用 useImperativeHandle 提供一个组合的对象,或者通过其他方式传递多个 ref(如回调模式)。
3. 函数组件上仍然不要直接传递 ref
即使使用了 forwardRef,也应当只将它用在转发场景中。对于普通的函数组件,依然不建议将其视为 DOM 元素来附加 ref,而应该设计好接口。
总结
forwardRef让函数组件有能力接收并转发ref,消除了封装组件时的最后一层障碍。- 与
useImperativeHandle配合,可以对外暴露可控的实例方法,保持组件封装性。 - 在 HOC、UI 库开发和复杂交互场景下,
forwardRef是确保 ref 正确传递的关键工具。
掌握 forwardRef,能让你的组件设计更加灵活、专业,更符合 React 组件化的深层逻辑。