React 中的 forwardRef:从入门到实战,轻松实现 Ref 转发

328 阅读5分钟

一、forwardRef 是什么?—— 一句话搞懂核心概念

在 React 中,ref 就像一个 “指针”,用于获取 DOM 元素或组件实例的引用。但遇到函数组件时,直接使用 ref 会 “失灵”,因为函数组件没有实例可引用。这时,forwardRef 就派上用场啦!它是 React 提供的一个高阶函数,专门用来将父组件的 ref 转发到子组件的 DOM 元素或类组件实例上,相当于给 ref 搭建了一条 “专属通道”~

1.1 核心语法:给组件装上 “转发器”

const 转发后的组件 = forwardRef((props, ref) => {
  // 子组件逻辑,将 ref 绑定到具体的 DOM 或类组件
  return <子组件 {...props} ref={ref} />;
});
  • 第一个参数是子组件接收的 props,和普通函数组件一致
  • 第二个参数 ref 是父组件传递的引用,需要显式绑定到子组件内部的 DOM 元素上

二、为什么需要 forwardRef?—— 解决函数组件的 ref 困境

先看一个 “踩坑” 案例:直接给函数组件传 ref 会报错!

// 子组件:普通函数组件
function InputComponent(props) {
  return <input type="text" />;
}

// 父组件:尝试用 ref 聚焦输入框
function App() {
  const inputRef = useRef(null);
  useEffect(() => {
    inputRef.current.focus(); // 报错!函数组件无法获取 ref
  }, []);
  return <InputComponent ref={inputRef} />; // 警告:Function components cannot be given refs
}

报错原因:React 规定,ref 只能直接绑定到 DOM 元素或类组件实例上,而函数组件没有实例,所以直接传 ref 会被忽略。
解决方案:用 forwardRef 包装子组件,让它具备转发 ref 的能力,就像给 ref 开了一条 “绿色通道”~

三、forwardRef 实战:从基础到进阶场景

3.1 基础用法:父组件获取子组件的 DOM 元素

结合你提供的代码,我们来实现一个 “父组件控制子组件输入框自动聚焦” 的功能:

import { useRef, useEffect, forwardRef } from 'react';
import './App.css';

// 子组件:接收 props 和 ref,将 ref 绑定到 input 上
function Guang(props, ref) { 
  // 这里的 ref 是父组件传来的,直接绑定给 input 元素
  return (
    <div>
      <input type="text" ref={ref} /> {/* ref 精准定位到 input */}
    </div>
  );
}

// 关键:用 forwardRef 包装组件,激活 ref 转发能力
const WrapperGuang = forwardRef(Guang);

function App() {
  const inputRef = useRef(null); // 创建 ref 引用

  // 组件挂载后自动聚焦输入框
  useEffect(() => {
    inputRef.current?.focus(); // 通过 ref 操作 DOM,和直接操作 input 一样方便
  }, []);

  return (
    <div className="App">
      {/* 给转发后的组件传 ref,就能拿到子组件内部的 input 了 */}
      <WrapperGuang ref={inputRef} />
    </div>
  );
}

export default App;

运行效果:页面加载后,输入框会自动获得焦点,是不是很神奇?😉
核心逻辑WrapperGuang 通过 forwardRef 获得了转发能力,父组件的 inputRef 跳过了组件层级,直接绑定到了子组件的 input 元素上。

3.2 高阶组件(HOC)中的 ref 转发

高阶组件(HOC)是包装组件的函数,但默认会 “拦截”ref,导致父组件拿不到内部组件的引用。比如下面这个 “日志高阶组件”:

// 高阶组件:打印 props 变化
function logProps(WrappedComponent) {
  // 问题:这里的 ref 会指向 LogProps 而不是 WrappedComponent
  return function LogProps(props) {
    console.log('当前 props:', props);
    return <WrappedComponent {...props} />;
  };
}

// 使用时,ref 无法指向内部的 Button
const LoggedButton = logProps(Button);
<LoggedButton ref={buttonRef} /> // buttonRef.current 是 LogProps 组件,不是 Button

解决办法:在 HOC 中用 forwardRef 转发 ref

function logProps(WrappedComponent) {
  // 用 forwardRef 接收父组件的 ref
  return forwardRef((props, ref) => {
    console.log('当前 props:', props);
    // 将 ref 转发给被包装的组件
    return <WrappedComponent {...props} ref={ref} />;
  });
}

// 现在 ref 能正确指向内部的 Button 了
const LoggedButton = logProps(Button);
<LoggedButton ref={buttonRef} /> // buttonRef.current 是 Button 的 DOM 或实例

3.3 嵌套组件:跨层级传递 ref 更优雅

如果组件嵌套多层(父 → 中 → 孙),想在父组件直接获取孙组件的 DOM,无需通过 props 层层传递 ref,用 forwardRef 就能轻松实现:

// 孙组件:绑定 ref 到 DOM
const GrandChild = forwardRef((props, ref) => {
  return <div ref={ref}>我是孙组件的 DOM</div>;
});

// 中间组件:直接转发 ref,无需处理 props
const Middle = forwardRef((props, ref) => {
  return <GrandChild ref={ref} />; // 把父组件的 ref 传给孙组件
});

// 父组件:直接操作孙组件的 DOM
function Parent() {
  const grandChildRef = useRef(null);
  useEffect(() => {
    console.log('孙组件的 DOM:', grandChildRef.current); // 成功获取!
  }, []);
  return <Middle ref={grandChildRef} />;
}

优势:中间组件无需关心 ref 的存在,像 “透明管道” 一样让 ref 直接到达目标,代码更简洁~

四、使用 forwardRef 的 3 个注意事项

4.1 ref 必须绑定到 “真实存在” 的目标上

ref 最终必须绑定到 DOM 元素(如 <div><input>)或类组件实例上,否则会无效:

// 错误:ref 没有绑定到具体 DOM
const BadComponent = forwardRef((props, ref) => {
  return <div>我没有绑定 ref</div>; // ref 没地方去,会是 null
});

// 正确:ref 绑定到 DOM 元素
const GoodComponent = forwardRef((props, ref) => {
  return <div ref={ref}>我绑定了 ref</div>; // 正确!
});

4.2 函数组件命名:方便调试

如果用匿名函数定义转发组件,React DevTools 会显示为 ForwardRef,不利于调试。建议用具名函数

// 不好:匿名函数,调试时看不清组件名
const Input = forwardRef((props, ref) => <input ref={ref} />);

// 好:具名函数,调试时显示为 ForwardRef(InputComponent)
const InputComponent = forwardRef(function InputComponent(props, ref) {
  return <input ref={ref} />;
});

4.3 别和 props 里的 “ref” 重名

子组件的 props 中不要用 ref 作为属性名,否则会和 forwardRef 传递的 ref 冲突:

// 错误:props 里的 ref 和 forwardRef 的 ref 重名
const BadComponent = forwardRef((props, ref) => {
  // 这里的 props.ref 和 ref 会混淆!
  return <div ref={ref}>{props.ref}</div>; 
});

// 正确:用其他名字(如 inputRef)
const GoodComponent = forwardRef((props, ref) => {
  return <div ref={ref}>{props.inputRef}</div>; 
});

五、总结:forwardRef 什么时候用?

简单说:当你需要让父组件的 ref 跳过中间组件,直接到达子组件内部的 DOM 或类组件时,就用 forwardRef

它的核心价值是:打破函数组件对 ref 的限制,让 ref 像 “靶向导弹” 一样精准到达目标,同时避免用 props 传递 ref 带来的代码冗余。

下次再遇到 “函数组件拿不到 ref” 的问题,记得用 forwardRef 这个神器哦~