函数组件拿不到 ref?forwardRef:这个 “传话筒” 能帮你

174 阅读5分钟

在 React 中,ref是个很实用的东西 —— 它能让我们直接访问 DOM 元素或组件实例,比如给输入框自动聚焦、获取元素尺寸等。但用着用着你会发现:类组件可以轻松接收ref,可函数组件却不行?每次尝试传递ref,控制台要么报错,要么ref.currentundefined

别慌,这不是函数组件的 “bug”,而是 React 的设计规范。今天就用一个实际例子,聊聊为什么函数组件不能直接接收ref,以及forwardRef是如何像 “传话筒” 一样解决这个问题的。

函数组件直接传 ref

// 子组件:一个简单的函数组件,想接收ref
function Guang(props, ref) {
  // 尝试打印ref,看看能不能拿到
  console.log("子组件的ref:", ref);
  return (
    <div>
      <input type="text" ref={ref} /> {/* 想把ref绑定到input上 */}
    </div>
  );
}

// 父组件:想通过ref控制子组件的input
function App() {
  const inputRef = useRef(null);

  // 组件挂载后,尝试让input自动聚焦
  useEffect(() => {
    inputRef.current?.focus(); // 这里会报错吗?
  }, []);

  return (
    <div>
      {/* 直接给函数组件传ref */}
      <Guang ref={inputRef} />
    </div>
  );
}

image.png

  • 父组件的useEffect里,inputRef.current也是undefined,调用focus()会报错。

这说明:函数组件默认不能接收ref参数,父组件传递的ref根本传不进去。

为什么函数组件不能直接接收 ref?

React 这么设计,其实是为了避免 “滥用 ref”。

函数组件的核心是 “无状态”(早期 React 函数组件没有state),它更像是一个 “渲染函数”—— 接收props,返回 JSX。而ref的作用是 “访问实例”,但函数组件本身没有实例(不像类组件有this),React 担心开发者会试图通过ref访问函数组件内部的状态或方法,这会破坏 React “单向数据流” 的设计理念(数据应该通过props传递,而不是直接操作组件内部)。

打个比方:函数组件就像一台自动售货机(只接受硬币props,吐出商品 JSX),而ref就像一把钥匙,React 不允许你直接用钥匙打开机器内部(避免乱改内部结构)。

forwardRef:给函数组件装个 “传话筒”

如果我们确实需要访问函数组件内部的 DOM 元素(比如让子组件的input自动聚焦),该怎么办?

React 提供了forwardRef—— 它的作用就像一个 “传话筒”,能把父组件的ref穿过函数组件,传递到组件内部的 DOM 元素上。

用法步骤:3 步搞定 ref 转发

步骤 1:用 forwardRef 包裹子组件

// 用forwardRef包裹函数组件,让它能接收ref
const WrapperGuang = forwardRef(Guang);

forwardRef接收一个函数组件作为参数,返回一个 “能接收 ref 的新组件”。

步骤 2:子组件接收 ref 参数

函数组件需要额外接收一个ref参数(作为第二个参数):

// 子组件:第一个参数是props,第二个参数是ref
function Guang(props, ref) {
  console.log("子组件收到的ref:", ref); // 这次能拿到ref了!
  return (
    <div>
      {/* 把ref绑定到内部的input上 */}
      <input type="text" ref={ref} />
    </div>
  );
}

步骤 3:父组件正常传递 ref

function App() {
  const inputRef = useRef(null);

  useEffect(() => {
    // 现在能拿到input的DOM节点了!
    inputRef.current?.focus(); // 成功自动聚焦
  }, []);

  return (
    <div>
      {/* 给forwardRef包装后的组件传ref */}
      <WrapperGuang ref={inputRef} />
    </div>
  );
}

image.png

  • 父组件的inputRef.current指向input的 DOM 节点,调用focus()成功让输入框自动聚焦。

原理:forwardRef 是如何 “转发” ref 的?

forwardRef的工作流程可以简单理解为:

  1. 父组件创建ref对象(inputRef = useRef(null));
  2. 父组件将ref传递给forwardRef包装后的组件(WrapperGuang);
  3. forwardRef把这个ref作为第二个参数,传递给原始函数组件(Guang);
  4. 原始函数组件将ref绑定到内部的 DOM 元素(input)上;
  5. 最终,父组件的inputRef.current指向这个input的 DOM 节点。

就像一个 “传话筒”:父组件把 “话”(ref)递给传话筒(forwardRef),传话筒再把 “话” 传给子组件,子组件听到后执行操作(绑定到 input)。

注意事项:这些细节别踩坑

  1. ref 是第二个参数,不能写在 props 里
    函数组件接收ref时,必须作为第二个参数,不能从props里解构(props.ref是拿不到的):

    // 错误写法:ref不在props里
    function Guang(props) {
      const { ref } = props; // 这样拿不到ref!
      return <input ref={ref} />;
    }
    
    // 正确写法:ref是第二个参数
    function Guang(props, ref) { // 单独接收ref
      return <input ref={ref} />;
    }
    
  2. forwardRef 不影响 props 的传递
    forwardRef只负责转发ref,不会干扰props的传递。父组件传递的其他属性,依然通过第一个参数接收:

    // 父组件传递props
    <WrapperGuang ref={inputRef} name="用户名" />
    
    // 子组件接收props和ref
    function Guang(props, ref) {
      console.log("props:", props.name); // 打印"用户名"
      return <input ref={ref} placeholder={props.name} />;
    }
    
  3. 类组件不需要 forwardRef
    forwardRef只用于函数组件。类组件本身可以直接接收ref(指向类实例),无需转发:

    // 类组件可以直接接收ref
    class ClassComponent extends React.Component {
      render() {
        return <input ref={this.props.innerRef} />;
      }
    }
    

什么时候需要用 forwardRef?

forwardRef的典型使用场景是:封装通用 UI 组件时,允许使用者访问组件内部的 DOM 元素

比如你封装了一个CustomInput组件,内部是一个input标签,其他开发者使用时可能需要聚焦、清空内容等操作。这时用forwardRef转发ref,能让使用者像操作原生input一样操作你的组件:

// 封装一个自定义输入框组件
const CustomInput = forwardRef((props, ref) => {
  return (
    <div className="custom-input">
      <input type="text" ref={ref} {...props} />
    </div>
  );
});

// 其他开发者使用时,能通过ref控制内部input
function User() {
  const inputRef = useRef(null);

  const handleClear = () => {
    inputRef.current.value = ""; // 清空输入框
  };

  return (
    <div>
      <CustomInput ref={inputRef} placeholder="请输入" />
      <button onClick={handleClear}>清空</button>
    </div>
  );
}

总结:forwardRef 的核心价值

forwardRef解决了函数组件无法接收ref的问题,让我们既能保持函数组件的简洁性,又能灵活访问其内部的 DOM 元素。它的核心价值是:在不破坏函数组件设计理念的前提下,安全地转发 ref,满足访问内部 DOM 的需求

记住一句话:当你需要让父组件访问函数组件内部的 DOM 元素时,forwardRef就是那个可靠的 “传话筒”。

最后,再看一眼完整的正确示例,巩固一下用法吧:

import { useRef, useEffect, forwardRef } from "react";

// 1. 定义原始函数组件,接收props和ref
function Guang(props, ref) {
  return <input type="text" ref={ref} />;
}

// 2. 用forwardRef包装,得到可接收ref的组件
const WrapperGuang = forwardRef(Guang);

// 3. 父组件使用
function App() {
  const inputRef = useRef(null);

  useEffect(() => {
    inputRef.current?.focus(); // 成功聚焦
  }, []);

  return <WrapperGuang ref={inputRef} />;
}

export default App;

下次封装函数组件时,别再为 “拿不到 ref” 发愁了 ——forwardRef这把 “钥匙”,能帮你打开函数组件内部 DOM 的大门~ 🚪