前言
在 React 的世界里,Props 是数据传递的主旋律。然而,有两位特殊的客人——key 和 ref,它们并不会被包含在 props 中。如果你尝试在普通函数组件中接收 ref,只会得到 undefined。为了解决这个问题,forwardRef 应运而生。
一、 产生背景:消失的 Ref
在 React 中,函数组件没有实例(Instance),因此父组件无法像操作 Class 组件那样通过 ref 直接获取函数组件的引用。此外,React 对 ref 做了特殊处理,它不会像普通属性那样通过 props 传递。
二、 核心概念:什么是 forwardRef?
forwardRef(引用转发)是一个高阶组件工具,它允许函数组件接收父组件传递的 ref,并将其进一步“转发”给内部的 DOM 元素或子组件。
语法定义
React.forwardRef((props,ref)=>{}),它会包裹一个渲染函数,这个渲染函数接受两个参数,第一个参数为props,第二个参数为父组件传递的ref
// 子组件:使用 forwardRef
const ChildComponent = React.forwardRef<HTMLInputElement, any>((props, ref) => {
// 将 ref 绑定到内部的 button 元素上
return <button ref={ref} {...props}>Click Me</button>;
});
// 父组件
const ParentComponent: React.FC = () => {
const buttonRef = useRef(null);
const handleClick = () => {
// 现在可以通过 ref 直接操作子组件内部的 button DOM 元素
buttonRef.current.focus();
};
return (
<>
<ChildComponent ref={buttonRef} />
<button onClick={handleClick}>Focus the Button</button>
</>
);
}
三、 实战:跨组件操控 DOM (TSX 实现)
这是 forwardRef 最经典的使用场景:父组件直接聚焦子组件内部的输入框。
import React, { useRef, forwardRef } from 'react';
// 1. 定义子组件 Props 类型
interface ChildProps {
label: string;
}
// 2. 使用 forwardRef 包裹渲染函数
// 第一个泛型是 Ref 类型,第二个是 Props 类型
const MyInput = forwardRef<HTMLInputElement, ChildProps>((props, ref) => {
return (
<div className="input-group">
<label>{props.label}</label>
{/* 3. 将接收到的 ref 绑定到具体的 DOM 元素上 */}
<input ref={ref} type="text" className="input-field" />
</div>
);
});
// 父组件
const Home: React.FC = () => {
const inputRef = useRef<HTMLInputElement>(null);
const handleFocus = () => {
// 4. 直接通过 current 操作子组件内部的 DOM
inputRef.current?.focus();
};
return (
<div style={{ padding: '20px' }}>
<MyInput ref={inputRef} label="用户名:" />
<button onClick={handleFocus} style={{ marginTop: '10px' }}>
点击聚焦子组件输入框
</button>
</div>
);
};
export default Home;
四、 进阶使用场景:暴露子组件方法
除了访问 DOM,我们还可以利用 forwardRef 配合 useImperativeHandle 来让父组件调用子组件内部定义的特定方法。
import React, { useImperativeHandle, forwardRef, useRef } from 'react';
// 定义子组件向外暴露的接口
export interface ChildActions {
scrollIntoView: () => void;
showMessage: () => void;
}
const FancyChild = forwardRef<ChildActions, {}>((props, ref) => {
const internalRef = useRef<HTMLDivElement>(null);
// 使用 useImperativeHandle 自定义暴露给父组件的内容
useImperativeHandle(ref, () => ({
scrollIntoView: () => {
internalRef.current?.scrollIntoView({ behavior: 'smooth' });
},
showMessage: () => {
alert("这是子组件内部的方法!");
}
}));
return <div ref={internalRef} style={{ height: '100px', background: '#eee' }}>我是子组件</div>;
});
const Parent: React.FC = () => {
const actionsRef = useRef<ChildActions>(null);
return (
<>
<FancyChild ref={actionsRef} />
<button onClick={() => actionsRef.current?.showMessage()}>
调用子组件自定义方法
</button>
</>
);
};
export default Parent;
五、 总结与注意事项
- 封装性原则:虽然
forwardRef很强大,但不要过度使用。只有在像 Input、Button 这种基础 UI 组件,或者必须手动控制 DOM(聚焦、滚动、测量)时才推荐使用。 - 高阶组件 (HOC) :如果你使用了高阶组件包裹了被
forwardRef的组件,确保 HOC 也转发了ref。