引言
在 React 开发中,useRef
和 forwardRef
是两个非常实用但又容易被忽视的 API。它们虽然不像 useState
或 useEffect
那样高频使用,但在处理 DOM 操作、组件间引用传递等场景中,却有着不可替代的作用。
本文将从基础概念讲起,逐步深入,结合代码示例和实战场景,带你全面掌握 useRef
和 forwardRef
的使用技巧与最佳实践。
一、什么是 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>
)
}
在这个例子中,我们使用 forwardRef
将ref
从父组件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)结合使用
在使用高阶组件封装组件时,如果不使用 forwardRef
,ref
会被包裹在 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开发中,useRef
和forwardRef
是一对非常实用的搭档。useRef
帮助我们管理组件内部的状态和DOM元素,而forwardRef
则解决了组件间引用传递的问题,使得组件更加灵活、复用性更强。