React 中的 React.forwardRef:从一脸懵到熟练用 🚀

79 阅读7分钟

一、先看个场景:你是不是也遇到过这种坑

image.png

假设你写了个超简单的输入框组件:

// 子组件:MyInput.jsx
const MyInput = (props) => {
  return <input {...props} />
}

// 父组件:App.jsx
function App() {
  const inputRef = React.useRef(null)
  
  // 想在组件挂载后自动聚焦输入框
  React.useEffect(() => {
    inputRef.current.focus() // 这里会报错!因为inputRef.current是null 😭
  }, [])
  
  return <MyInput ref={inputRef} />
}

运行之后控制台直接红了 ——"Cannot read properties of null (reading 'focus')"。这时候你可能会挠头:明明给子组件传了 ref,怎么就是拿不到 DOM 元素?

别急!这不是你的错,而是 React 的 "安全机制"——默认情况下,函数组件就像个害羞的小姑娘,会把 ref 拒之门外。这时候就该forwardRef登场了,它就像个贴心的快递员,能帮你把 ref 稳稳地送到子组件内部 📦

二、先搞懂:React 里的 ref 到底是啥?

image.png

在讲forwardRef之前,得先明白ref的基础用法。你可以把ref理解成 "DOM 元素的身份证"—— 有了它,你就能直接找到并操作对应的 DOM 元素。

(1)ref 能做啥?

  • 让输入框自动聚焦(就像登录页点进去自动光标闪烁)
  • 获取输入框的实时值(不用通过 onChange 层层传递)
  • 操作 DOM 的样式或属性(比如滚动到指定位置)
  • 调用子组件的方法(比如让弹窗组件关闭)

(2)基础用法(给 DOM 元素挂 ref)

function App() {
  // 1. 创建一个ref容器
  const inputRef = React.useRef(null)
  
  // 3. 在合适的时机使用ref
  const handleFocus = () => {
    inputRef.current.focus() // 调用DOM原生方法
    console.log('当前输入值:', inputRef.current.value)
  }
  
  // 2. 把ref挂到DOM元素上
  return (
    <div>
      <input ref={inputRef} placeholder="点击按钮聚焦我" />
      <button onClick={handleFocus}>点我聚焦</button>
    </div>
  )
}

这种直接给 DOM 元素挂 ref 的方式很简单,但如果这个 input 被封装成了组件,问题就来了 —— 刚才的例子已经证明:函数组件默认不接收 ref

三、forwardRef:给函数组件开个 "ref 通道"

image.png

forwardRef(中文可以叫 "转发 ref")是 React 提供的高阶组件,它的核心作用就一个:让函数组件能接收 ref,并把 ref 转发给内部的 DOM 元素

(1)基本用法(解决开头的坑)

import React, { forwardRef } from 'react'

// 用forwardRef包裹函数组件,就能接收ref参数了
const MyInput = forwardRef((props, ref) => {
  // 把接收到的ref转发给内部的input元素
  return <input {...props} ref={ref} />
})

function App() {
  const inputRef = React.useRef(null)
  
  React.useEffect(() => {
    inputRef.current.focus() // 现在能正常聚焦了!🎉
  }, [])
  
  return <MyInput ref={inputRef} placeholder="我能自动聚焦啦" />
}

这段代码的关键变化:

  1. 子组件用forwardRef包裹后,参数变成了(props, ref)(注意第二个参数才是 ref)
  2. 子组件内部把 ref 绑到了真正的 DOM 元素上(这里是 input)
  3. 父组件的 ref 最终指向的是子组件内部的 input,而不是子组件本身

(2)为什么需要 forwardRef?

React 这么设计是有原因的:函数组件本身没有实例(不像类组件有 this),如果直接给函数组件传 ref,React 也不知道该指向哪里。而forwardRef相当于告诉 React:"你把 ref 传给我,我知道该给谁"。

举个生活例子:你想给朋友的孩子送礼物(ref),但你只认识朋友(子组件),不认识孩子(内部 DOM)。这时候forwardRef就像朋友说:"把礼物给我,我转交给孩子"。

四、哪些场景必须用 forwardRef?

image.png

不是所有组件都需要forwardRef,这几种情况一定要用:

(1)需要父组件操作子组件内部 DOM

最常见的就是 "自动聚焦" 需求,比如:

  • 登录页加载完成后,光标自动定位到用户名输入框

  • 弹窗打开后,输入框自动获得焦点

  • 表单验证失败时,自动聚焦到错误的输入框

// 带自动聚焦的输入框组件
const FocusableInput = forwardRef((props, ref) => {
  return <input {...props} ref={ref} />
})

// 使用组件的页面
function LoginPage() {
  const usernameRef = React.useRef(null)
  
  // 页面加载完成后聚焦用户名输入框
  React.useEffect(() => {
    usernameRef.current.focus()
  }, [])
  
  return (
    <div>
      <FocusableInput ref={usernameRef} placeholder="用户名" />
      <FocusableInput placeholder="密码" type="password" />
    </div>
  )
}

(2)封装通用组件时暴露内部 DOM

如果你封装了一个 UI 组件库(比如自定义下拉框、日期选择器),用户可能需要通过 ref 获取内部 DOM 的位置或尺寸。这时候用forwardRef把 ref 转发到关键 DOM 上,会让组件更易用。

// 封装一个带图标的输入框组件
const IconInput = forwardRef(({ icon, ...props }, ref) => {
  return (
    <div style={{ display: 'flex', alignItems: 'center' }}>
      <span>{icon}</span>
      {/* 把ref转发给实际输入框,方便外部操作 */}
      <input {...props} ref={ref} style={{ marginLeft: 8 }} />
    </div>
  )
})

// 使用时能直接操作内部input
function SearchBar() {
  const inputRef = React.useRef(null)
  
  const handleSearch = () => {
    const value = inputRef.current.value
    console.log('搜索内容:', value)
  }
  
  return (
    <div>
      <IconInput 
        ref={inputRef} 
        icon="🔍" 
        placeholder="输入关键词搜索" 
      />
      <button onClick={handleSearch}>搜索</button>
    </div>
  )
}

五、这些坑千万别踩!(注意事项)

image.png

(1)函数组件必须用 forwardRef 才能接 ref

这是新手最容易犯的错!直接在函数组件上用 ref 会被忽略:

// 错误示例:没包forwardRef,ref会无效
const BadInput = (props) => {
  return <input {...props} />
}

function App() {
  const ref = React.useRef(null)
  return <BadInput ref={ref} /> // 这里的ref不会生效!⚠️
}

(2)参数顺序不能错

forwardRef包裹的组件,第一个参数是props,第二个才是ref,不能搞反:

// 正确写法
const GoodComponent = forwardRef((props, ref) => { ... })

// 错误写法(ref位置错了)
const BadComponent = forwardRef((ref, props) => { ... }) // ❌

(3)ref 最终指向你绑定的元素

很多人以为 ref 会指向子组件本身,其实不是 ——ref 指向的是你在子组件里最终绑定的那个元素:

const MyComponent = forwardRef((props, ref) => {
  return (
    <div>
      {/* ref绑给了p标签,所以父组件拿到的就是p标签 */}
      <p ref={ref}>我是被ref绑定的元素</p>
      <input placeholder="我没被绑定,拿不到哦" />
    </div>
  )
})

(4)类组件不需要 forwardRef

forwardRef只给函数组件用,类组件本身就能接收 ref:

// 类组件直接能用ref,不用forwardRef
class ClassComponent extends React.Component {
  render() {
    return <input />
  }
}

function App() {
  const ref = React.useRef(null)
  return <ClassComponent ref={ref} /> // 完全没问题
}

六、结合项目代码深入理解

image.png

我们来看一个实际项目中的例子(就是你提供的代码):

import React, { forwardRef, useRef, useEffect } from 'react'

// 定义子组件
function Guang(props, ref) {
  return (
    <div>
      {/* 把ref绑到input上 */}
      <input type="text" ref={ref} />
    </div>
  )
}

// 用forwardRef包装,让它能接收ref
const WrappedGuang = forwardRef(Guang);

// 父组件
function App() {
  // 创建ref
  const inputRef = useRef(null);
  
  // 组件挂载后让input自动聚焦
  useEffect(() => {
    inputRef.current?.focus(); // 这里的current就是子组件里的input
  }, []);
  
  // 给子组件传ref
  return <WrappedGuang title="Xiang" ref={inputRef} />;
}

这段代码的执行流程就像这样:

  1. 父组件创建inputRef → 传给WrappedGuang组件

  2. forwardRefinputRef转发给Guang组件的第二个参数

  3. Guang组件把ref绑到内部的input元素上

  4. 父组件通过inputRef.current就能操作这个input

如果没有forwardRef会怎样?——inputRef.current会是undefined,因为函数组件默认不接收 ref。就像快递员(ref)到了小区门口(子组件),发现没人接,只能原路返回。

七、进阶:配合 useImperativeHandle 自定义暴露内容

image.png

有时候你不想让父组件直接操作 DOM,而是想暴露特定方法(比如 "清空输入框"、"验证内容"),这时候可以用useImperativeHandle配合forwardRef

它的作用是:自定义通过 ref 暴露给父组件的内容(默认暴露的是整个 DOM 元素)。

import React, { forwardRef, useRef, useImperativeHandle } from 'react'

const CustomInput = forwardRef((props, ref) => {
  // 内部真实的DOM ref
  const inputRef = useRef(null)
  
  // 自定义暴露给父组件的方法
  useImperativeHandle(ref, () => ({
    // 只暴露需要的方法,而不是整个DOM
    focus: () => {
      inputRef.current.focus()
    },
    clear: () => {
      inputRef.current.value = ''
    },
    // 甚至可以返回处理后的值
    getValue: () => {
      return inputRef.current.value.trim() // 自动去除空格
    }
  }))
  
  return <input {...props} ref={inputRef} />
})

// 父组件使用
function App() {
  const inputRef = useRef(null)
  
  return (
    <div>
      <CustomInput placeholder="试试下面的按钮" />
      <button onClick={() => inputRef.current.focus()}>聚焦</button>
      <button onClick={() => inputRef.current.clear()}>清空</button>
      <button onClick={() => alert(inputRef.current.getValue())}>获取值</button>
    </div>
  )
}

这样做的好处是:

  • 隐藏内部实现细节(父组件不用知道你用的是 input 还是 textarea)
  • 控制暴露范围(避免父组件乱改 DOM 属性)
  • 提供更友好的 API(比如 getValue 自动处理空格)

八、总结:forwardRef 一句话总结

image.png

forwardRef就像 React 组件间的 "ref 快递中转站"—— 让父组件的 ref 能穿过函数组件,准确送到内部的 DOM 元素手上。当你需要在父组件操作子组件内部 DOM 时,它就是最佳选择。

最后再给新手朋友一个小建议:刚开始用的时候可以多打印ref.current看看里面是什么(console.log(ref.current)),直观感受 ref 的指向变化。用多了就会发现,这东西其实一点也不难!😉

如果觉得有收获,欢迎点赞收藏~有问题可以在评论区交流哦!