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则解决了组件间引用传递的问题,使得组件更加灵活、复用性更强。


参考资料

相关推荐
码界奇点9 分钟前
基于Vue3与TypeScript的后台管理系统设计与实现
前端·javascript·typescript·vue·毕业设计·源代码管理
ashcn200114 分钟前
水滴按钮解析
前端·javascript·css
攀登的牵牛花14 分钟前
前端向架构突围系列 - 框架设计(五):契约继承原则
前端·架构
爱吃奶酪的松鼠丶21 分钟前
React长列表,性能优化。关于循环遍历的时候,key是用对象数据中的ID还是用索引
javascript·react.js·性能优化
豆苗学前端1 小时前
你所不知道的前端知识,html篇(更新中)
前端·javascript·面试
一 乐1 小时前
绿色农产品销售|基于springboot + vue绿色农产品销售系统(源码+数据库+文档)
java·前端·数据库·vue.js·spring boot·后端·宠物
zzjyr1 小时前
Webpack 生命周期原理深度解析
前端
xiaohe06011 小时前
💘 霸道女总裁爱上前端开发的我
前端·游戏开发·trae
sophie旭1 小时前
内存泄露排查之我的微感受
前端·javascript·性能优化
k***1951 小时前
Spring 核心技术解析【纯干货版】- Ⅶ:Spring 切面编程模块 Spring-Instrument 模块精讲
前端·数据库·spring