在 React 开发中,我们经常会遇到这样一个需求:如何让父组件直接访问子组件内部的 DOM 元素或组件实例?例如,点击按钮时自动聚焦某个输入框,或者直接调用子组件的方法。
当你面对这样的需求时,你第一时间可能会想到组件通信的方式,通过
props
传递数据和回调函数,然而,组件通信并不能满足直接访问子组件内部 DOM 或实例的需求。为了解决这个问题,React 提供了
React.forwardRef
这一特殊机制,本文将从基础概念出发,结合代码示例,带你了解forwardRef
的使用方法。
forwardRef
?在实际开发中,我们常会遇到这类需求:
reset()
方法重置表单状态。此时,你的第一反应可能是通过 props + callback 的方式传递数据或触发行为,但这类需求的本质与“数据传递”无关,而是需要 直接访问 DOM 或组件实例本身。
React 推崇的组件通信方式(如 props 传递)遵循单向数据流,它适合处理数据共享和事件回调,但当需求涉及直接操作 DOM 或调用组件方法时,传统的组件通信就无能为力了。
例如,以下代码尝试通过 props 传递一个聚焦函数:
function InputComponent({ onFocus }) { return ; } function App() { const handleFocus = () => { // ❌ 无法直接操作 input 元素 console.log('触发了聚焦'); }; return
这段代码中,虽然 handleFocus
会被调用,但却无法真正实现“自动聚焦”的效果,因为这种方式只是通知子组件“应该聚焦”,而 没有提供访问 DOM 的途径。
ref
与 forwardRef
ref
的概念ref
是 React 提供的“特殊通道”,它允许开发者 直接访问 DOM 元素或组件实例,一般情况下,它适用于处理以下问题:
focus()
。例如,通过 ref
直接操作 DOM 元素:
function App() { const inputRef = useRef(null); const handleFocus = () => { inputRef.current.focus(); // ✅ 直接调用 DOM 方法 }; return (
forwardRef
?上述代码在 类组件 中运行良好,但遇到 函数组件 时就会报错:
// 报错:Function components cannot be given refs function InputComponent() { return ; } function App() { const inputRef = useRef(null); return
问题在于:函数组件本质上是纯函数,没有实例。React 不允许直接为函数组件绑定 ref
。
forwardRef
的诞生为了解决这一限制,React 提供了 forwardRef
:
ref
给内部的 DOM 元素或子组件。例如,通过 forwardRef
实现跨组件访问:
const InputComponent = forwardRef((props, ref) => ( // ✅ 将 ref 转发给 input )); function App() { const inputRef = useRef(null); useEffect(() => { inputRef.current.focus(); // ✅ 成功聚焦 }, []); return
这是一个表单页面:在用户点击“提交”后,如果验证失败,我们希望自动将焦点跳转到第一个错误的输入框。但这个输入框位于某个函数组件内部,传统方式无法直接操作。
如果子组件是类组件,以下代码可以正常工作:
class InputComponent extends React.Component { render() { return ; } }
父组件通过 ref
直接调用 focus()
:
function App() { const inputRef = useRef(null); useEffect(() => { inputRef.current.focus(); // ✅ 成功聚焦 }, []); return
但若子组件是函数组件,这段代码就会报错:
function InputComponent() { return ; // ❌ 报错:Function components cannot be given refs }
问题在于:函数组件没有实例,React 无法将 ref
绑定到它身上。
通过 forwardRef
,我们可以让函数组件“接收” ref
,并将其转发给内部的 DOM 元素,
实现效果:
// 子组件:定义一个函数组件并使用 forwardRef 接收 ref const InputComponent = React.forwardRef((props, ref) => ( // ✅ 将 ref 赋值给 input )); // 父组件:通过 ref 控制子组件的 input 元素 function App() { const inputRef = useRef(null); // 创建 ref 对象 useEffect(() => { inputRef.current?.focus(); // ✅ 页面加载后自动聚焦 }, []); return
子组件通过 forwardRef
接收父组件传递的 ref
,并将其绑定到 元素上。
父组件通过 useRef
创建 ref
,并在 useEffect
中调用 focus()
方法。
效果:页面加载时,子组件的输入框自动获得焦点,无需修改子组件内部逻辑。
ref
和 props
在实际开发中,我们经常需要同时传递 ref
和普通属性,例如,定义一个带占位符的输入框组件,同时允许父组件控制其聚焦状态。
实现效果:
focus()
或 blur()
方法。// 子组件:同时接收 ref 和 props const ForwardedInput = React.forwardRef((props, ref) => ( )); // 父组件:传递 ref 和 props function App() { const inputRef = useRef(null); return (
子组件通过 props
接收占位符等常规属性,通过 ref
接收对 DOM 的引用。
父组件可以同时传递两者,实现“配置 + 操作”的双重控制。
效果:输入框显示占位符,父组件仍可通过 ref
调用 focus()
或其他 DOM 方法。
ref
在复杂组件结构中,父组件可能需要访问最底层的 DOM 元素,例如,一个嵌套多层的表单组件,父组件需要直接聚焦到某个深层的输入框。
实现效果
// 最底层的子组件:接收 ref 并赋值给 input const Input = React.forwardRef((props, ref) => ( )); // 中间组件:直接传递 ref const Wrapper = React.forwardRef((props, ref) => (
Wrapper
)通过 forwardRef
接收 ref
,并将其传递给子组件 Input
。Input
的存在,只需传递 ref
给 Wrapper
。input
元素,即使组件层级很深。forwardRef
与组件通信的对比对比维度 | forwardRef |
组件通信(props/callback) |
---|---|---|
核心目的 | 直接访问 DOM 或组件实例 | 数据共享与行为传递 |
实现方式 | 绕过 props,直接操作 | 通过 props 传递数据和回调函数 |
适用场景 | 聚焦输入框、调用子组件方法、强制动画 | 表单值传递、事件回调、状态共享 |
优缺点 | ✅ 可跨层级访问 DOM ❌ 破坏封装性 |
✅ 符合单向数据流 ❌ 无法直接操作 DOM |
优先使用组件通信:大部分需求可通过 props 和 callback 实现。
谨慎使用 forwardRef
:仅在需要直接操作 DOM 或组件实例时使用。
保持组件封装性:即使使用 forwardRef
,也应限制暴露的 DOM 范围。