React 中的 useRef 与 forwardRef:深入理解与实战应用

引言

在 React 开发中,useRefforwardRef 是两个非常实用但又容易被忽视的 API。它们虽然不像 useStateuseEffect 那样高频使用,但在处理 DOM 操作、组件间引用传递等场景中,却有着不可替代的作用。

本文将从基础概念讲起,逐步深入,结合代码示例和实战场景,带你全面掌握 useRefforwardRef 的使用技巧与最佳实践。

一、什么是 useRef?

useRef 是 React 提供的一个 Hook,用于在函数组件中创建一个"可变的引用对象",其.current属性被初始化为传入的参数(通常为null)。它在组件的整个生命周期中保持不变,不会引起组件重新渲染。

常见用途:

  • 访问 DOM 元素:比如聚焦输入框、获取元素尺寸等。
  • 保存可变状态:类似于类组件中的实例属性,适用于不触发重新渲染的变量。
  • 跨渲染周期保存数据:避免在每次渲染中重新创建某些对象。

示例代码:

jsx 复制代码
import { useRef, useEffect } from 'react'

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

  useEffect(() => {
    inputRef.current?.focus()
  }, [])

  return (
    <div>
      <input ref={inputRef} />
    </div>
  )
}

在这个例子中,我们使用 useRef 获取了输入框的引用,并在组件挂载后自动聚焦。

二、为什么需要 forwardRef?

React中默认情况下,ref 是不能传递给子组件的 。也就是说,如果你在父组件中通过 ref 引用了一个子组件,这个ref并不会自动绑定到子组件内部的 DOM 元素上。

这在某些场景下会带来不便,例如你想在父组件中访问子组件内部的某个输入框或按钮,这时候就需要使用 forwardRef

三、什么是 forwardRef?

forwardRef 是一个 React 高阶组件,用于将 ref 显式地传递给子组件,从而让子组件可以接收并使用这个 ref

它接受一个组件作为参数,并返回一个新的组件,该组件可以接收 ref 属性并将其传递给内部的 DOM 元素或其他子组件。

示例代码:

jsx 复制代码
import { forwardRef } from 'react'

const Light = forwardRef((props, ref) => {
  return (
    <div>
      <input type="text" ref={ref} />
    </div>
  )
})

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

  useEffect(() => {
    inputRef.current?.focus()
  }, [])

  return (
    <div>
      <Light ref={inputRef} />
    </div>
  )
}

在这个例子中,我们使用 forwardRefref 从父组件App传递给了子组件Light,并最终绑定到了 <input>元素上,实现了聚焦功能。

四、useRef + forwardRef 实战:封装可复用组件

在实际开发中,我们经常需要封装一些可复用的 UI 组件,并希望父组件可以访问组件内部的某些 DOM 元素。此时,forwardRef 就派上了用场。

场景描述:

我们封装一个 CustomInput 组件,希望父组件可以通过 ref 调用其内部 <input>focus() 方法。

步骤一:定义组件

jsx 复制代码
import { forwardRef } from 'react'

const CustomInput = forwardRef((props, ref) => {
  return (
    <div className="custom-input">
      <input type="text" ref={ref} />
    </div>
  )
})

步骤二:在父组件中使用

jsx 复制代码
function App() {
  const inputRef = useRef(null)

  useEffect(() => {
    inputRef.current?.focus()
  }, [])

  return (
    <div>
      <CustomInput ref={inputRef} />
    </div>
  )
}

这样,我们就成功地将 ref 从父组件传递到了子组件,并最终作用在了 <input> 上。

五、forwardRef 与高阶组件(HOC)结合使用

在使用高阶组件封装组件时,如果不使用 forwardRefref 会被包裹在 HOC 内部,导致父组件无法直接访问原始组件的 DOM。

示例:使用 forwardRef 包裹 HOC

定义输入框为组件

jsx 复制代码
function Light (props, ref) {
  console.log(props, ref);
  
  return (
    <div>
      <input type="text" />
    </div>
  )
}

  return (
    <div className='App'>
      {/* <input ref={ref} /> */}
      <Light ref={ref} />
      {/* <WrapperLight ref={ref} /> */}
    </div>
  )

这时候代码当中ref是不具有向下传递的能力的,那么如何使其具有向下传递的能力呢。

jsx 复制代码
import { 
  useRef,
  forwardRef
} from 'react'
// 返回一个全新的组件, ref 会被传递给新组件
const WrapperLight = forwardRef(Light)

  return (
    <div className='App'>
      {/* <input ref={ref} /> */}
      {/* <Light ref={ref} /> */}
      <WrapperLight ref={ref} />
    </div>
  )
}

export default App

现在咱们就在这里成功的从父组件当中拿到了子组件传递过来的ref值

完整源码

jsx 复制代码
import { 
  useRef,
  useEffect,
  forwardRef
} from 'react'

import './App.css'

function Light (props, ref) {
  console.log(props, ref);
  
  return (
    <div>
      <input type="text" ref={ref} />
    </div>
  )
}
// 返回一个全新的组件, ref 会被传递给新组件
const WrapperLight = forwardRef(Light)

function App() {
  // 父组件 ref 
  const ref = useRef(null)
  console.log(ref.current);

  useEffect(() =>{
    ref.current?.focus()
  },[])
  

  return (
    <div className='App'>
      {/* <input ref={ref} /> */}
      {/* <Light ref={ref} /> */}
      <WrapperLight ref={ref} />
    </div>
  )
}

export default App

六、最佳实践与注意事项

1. 不要滥用 ref

  • ref 应用于非受控组件或需要直接访问 DOM 的场景。
  • 尽量使用 React 的声明式方式(如 useState)来管理状态,而不是频繁操作 DOM。

2. 使用可选链运算符提升代码安全性

当访问 ref.current 时,使用可选链操作符 ?. 可以避免空值导致的错误:

jsx 复制代码
useEffect(() => {
  inputRef.current?.focus()
}, [])

3. forwardRef 仅适用于函数组件

  • 如果你使用的是类组件,可以使用 React.createRef()ref 回调函数。
  • forwardRef 是为函数组件设计的,是 React 推荐的方式。

七、总结

API 作用 使用场景
useRef 创建一个可变引用对象,跨渲染周期保持数据 操作 DOM、保存状态
forwardRef 将 ref 传递给子组件 组件封装、高阶组件中访问内部 DOM

在React开发中,useRefforwardRef是一对非常实用的搭档。useRef帮助我们管理组件内部的状态和DOM元素,而forwardRef则解决了组件间引用传递的问题,使得组件更加灵活、复用性更强。


参考资料

相关推荐
狗头大军之江苏分军19 小时前
iPhone 17 vs iPhone 17 Pro:到底差在哪?买前别被忽悠了
前端
小林coding19 小时前
再也不怕面试了!程序员 AI 面试练习神器终于上线了
前端·后端·面试
文心快码BaiduComate19 小时前
WAVE SUMMIT深度学习开发者大会2025举行 文心大模型X1.1发布
前端·后端·程序员
babytiger19 小时前
python 通过selenium调用chrome浏览器
前端·chrome
passer98119 小时前
基于webpack的场景解决
前端·webpack
奶昔不会射手19 小时前
css3之grid布局
前端·css·css3
举个栗子dhy19 小时前
解决在父元素上同时使用 onMouseEnter和 onMouseLeave时导致下拉菜单无法正常展开或者提前收起问题
前端·javascript·react.js
Coding_Doggy19 小时前
苍穹外卖前端Day1 | vue基础、Axios、路由vue-router、状态管理vuex、TypeScript
前端