虽然写了很久的React,但是对 useRef 的认识还是不够深入,所以写一篇笔记,记录一下。

React useRef 笔记
2231 words
... views

本文记录我在学习 React 的过程中,所认识到的一些与 useRef 相关的知识,如果其中有什么不对的地方,可以给我发邮件指正,或者在评论区留言,谢谢。

什么是 useRefh2

useRef 是一个 React Hook,它允许你引用一个不需要用于视图渲染的值。你可以把它生动地看作是一个在组件的整个生命周期内始终保持持久化的小盒子。

每一次调用,它都会返回一个包含 current 属性的可变对象。

const ref = useRef(initialValue)
// ref 的结构:{ current: initialValue }

核心特性h2

  • 持久化存储:在组件的后续渲染中,useRef 将始终返回同一个对象引用。
  • 不触发重渲染:更改 ref.current 属性不会触发组件的重新渲染。这使得它非常适合存储那些不影响视图的数据(如定时器 ID)。
  • DOM 访问:最常见的用法是将其作为 ref 属性传递给 JSX 节点,React 会自动将 DOM 节点赋值给 current
NOTE

这里的介绍仅作为简要概览,更详尽的 API 说明建议直接查阅 React 官方文档

常见用法h2

在了解了 useRef 的基础之后,下面进入几个在实际业务里更常见的用法。

存储定时器 IDh3

因为 useRef 的变化绝不会触发重渲染,所以它天生就适合用来“藏”那些不需要影响 UI 显示的内部状态数据,比如定时器的 ID。

import { useRef } from 'react'
function MyComponent() {
const intervalRef = useRef(null)
useEffect(() => {
intervalRef.current = setInterval(() => {
console.log('设置定时器')
}, 1000)
// 清理函数
return () => {
clearInterval(intervalRef.current)
}
}, []) // 空依赖数组表示只在组件挂载时执行
return <div>{/* 组件内容 */}</div>
}

获取列表 DOMh3

在 React 中,useRef 通常用于引用单个 DOM 元素。如果你需要获取一个列表(通过 .map() 渲染)中所有子元素的 DOM,不能简单地把同一个 ref 赋给它们,因为后面的元素会覆盖前面的。

最佳实践是使用 useRef 存储一个 Map 对象,并通过回调 Ref (Callback Ref) 将每个 DOM 节点存入这个 Map 中。

这种方法最稳健,因为它可以通过唯一的 ID 准确找到对应的 DOM,即使列表发生排序或增删,引用关系也不会乱。

最佳实践

推荐使用 useRef 存储一个 Map 对象,并通过回调 Ref (Callback Ref) 将每个 DOM 节点动态存入这个 Map 中。

CatList.jsx
import { useRef } from 'react'
function CatList() {
const itemsRef = useRef(null)
// 惰性初始化
if (itemsRef.current === null) {
itemsRef.current = new Map()
}
const cats = [
{ id: 1, name: 'Tom' },
{ id: 2, name: 'Jerry' },
{ id: 3, name: 'Garfield' },
]
function scrollToCat(catId) {
// 3. 使用 Map 获取指定 ID 的 DOM 节点
const map = itemsRef.current
const node = map.get(catId)
if (node) {
node.scrollIntoView({
behavior: 'smooth',
block: 'nearest',
inline: 'center',
})
// 也可以做其他操作,比如改变样式
node.style.backgroundColor = 'yellow'
setTimeout(() => (node.style.backgroundColor = ''), 1000)
}
}
return (
<div>
<nav>
<button onClick={() => scrollToCat(1)}>找 Tom</button>
<button onClick={() => scrollToCat(2)}>找 Jerry</button>
<button onClick={() => scrollToCat(3)}>找 Garfield</button>
</nav>
<ul>
{cats.map((cat) => (
<li
key={cat.id}
// 2. 使用回调 Ref
ref={(node) => {
const map = itemsRef.current
if (node) {
// 挂载时:存入 Map
map.set(cat.id, node)
} else {
// React18.x 卸载时:node 为 null,从 Map 中移除
map.delete(cat.id)
}
// React 19 会在卸载时执行这个函数
return () => {
map.delete(cat)
}
}}
>
{cat.name}
</li>
))}
</ul>
</div>
)
}
export default CatList

自定义组件场景h4

如果你的列表项不是原生的 HTML 标签(如 <li>),而是自定义组件(如 <MyListItem />),你需要确保子组件使用了 forwardRef,否则 ref 无法透传到内部的 DOM 节点。

  1. 子组件须使用 forwardRef 暴露 DOM:
MyListItem.jsx
import { forwardRef } from 'react'
const MyListItem = forwardRef((props, ref) => {
return <li ref={ref}>{props.children}</li>
})
export default MyListItem
  1. 父组件使用回调分配 Map 保持不变:
Parent.jsx
// ... map 循环中,将 ref 传递给自定义组件
<MyListItem
key={cat.id}
ref={(node) => {
/* 和上面原生标签相同的 Map 存取逻辑 */
}}
>
{cat.name}
</MyListItem>
小结与规范
  • 不要尝试创建一个 Ref 的数组(如 [ref1, ref2]),这在 Hook 中很容易引发引用过期的问题,管理起来也十分棘手。
  • 永远推荐使用 useRef(new Map()) 配合回调 Ref:ref={node => map.set(id, node)}
  • 这种模式在处理各种动态复杂列表(例如:无限滚动 Infinite Scroll、拖拽排序、虚拟列表)时,既高效又符合 React 最佳实践标准。

解决“闭包陷阱”h3

setTimeoutsetInterval 或是原生事件监听器中,我们常常会遭遇一个令人头疼的问题:总是读取到“旧”的 State 值(也就是经典的闭包陷阱)。

典型痛点场景

假设你需要做一个“发送消息”的功能,用户点击发送后,系统会倒数“3秒后发送”。如果用户在这 3 秒内又急忙修改了输入框的消息内容,系统期望发送的应该是修改后的最新内容,而不是点击发送那一刻的旧内容。

import { useState, useRef, useEffect } from 'react'
function MessageSender() {
const [message, setMessage] = useState('')
// 关键:创建一个 Ref 来“镜像”最新的 message
const latestMessageRef = useRef('')
// 每次渲染,都把最新的 state 同步给 ref
// 这步操作是同步的,且不会触发副作用
useEffect(() => {
latestMessageRef.current = message
}, [message])
const handleSend = () => {
setTimeout(() => {
// 错误写法:console.log(message); // 这里永远是3秒前的值(闭包陷阱)
// 正确写法:读取 Ref
alert(`发送消息: ${latestMessageRef.current}`)
}, 3000)
}
return (
<div className="p-4 border rounded">
<h3>场景:解决异步闭包问题</h3>
<input value={message} onChange={(e) => setMessage(e.target.value)} placeholder="输入消息..." />
<button onClick={handleSend}>3秒后发送</button>
<p className="text-sm text-gray-500">点击发送后,试着立刻修改输入框内容</p>
</div>
)
}

usePrevious:记录上次值h3

随着重新渲染,React 的机制只负责告诉你现在的 State 是多少,却不会主动保留“上一次”是多少。

场景需求

设想我们正在开发一个股票趋势或数字看板。当最新数字变大时,我们需要显示带有绿色箭头的上涨特效,变小时显示红色的下跌特效。这就必须对比 current (当前值) 和 prev (上一次的值)。

import { useState, useEffect, useRef } from 'react'
// 封装成一个通用的 Hook
function usePrevious(value) {
const ref = useRef()
useEffect(() => {
ref.current = value // 在渲染完成后,记录当前值,供下一次渲染使用
}, [value])
return ref.current // 返回的是“上一次”的值
}
function StockTicker() {
const [price, setPrice] = useState(100)
const prevPrice = usePrevious(price) // 获取上一次的价格
// 计算趋势
let trend = ''
if (prevPrice && price > prevPrice) trend = '涨了'
if (prevPrice && price < prevPrice) trend = '跌了'
return (
<div className="p-4 border rounded mt-4">
<h3>场景:记录上一次的值</h3>
<p>当前价格: ${price}</p>
<p>上次价格: ${prevPrice}</p>
<p>趋势: {trend}</p>
<button onClick={() => setPrice((p) => p + 10)}>加价</button>
<button onClick={() => setPrice((p) => p - 10)}>降价</button>
</div>
)
}

useUpdateEffect:跳过首渲染h3

众所周知,useEffect 默认在组件挂载(Mount)时也会执行一次。但很多时候,我们只想在数据更新(Update)时执行逻辑,也就是要静默跳过首次加载的触发。

场景需求

配置自动保存草稿功能。当用户刚进入页面时(首次渲染),表单往往是空的或包含默认值,你绝不想此时去触发一次无用的“保存草稿” API 请求;只有当用户真正修改了内容(后续渲染)后,才应该触发保存逻辑。

import { useState, useEffect, useRef } from 'react'
function AutoSaveForm() {
const [text, setText] = useState('')
const isMountedRef = useRef(false) // 标记是否已经挂载
useEffect(() => {
// 如果是第一次渲染,将标记设为 true,然后直接结束
if (!isMountedRef.current) {
isMountedRef.current = true
return
}
// 从第二次渲染开始,才会执行下面的逻辑
console.log('正在保存草稿到服务器...', text)
}, [text])
return (
<div className="p-4 border rounded mt-4">
<h3>场景:跳过首次渲染 (AutoSave)</h3>
<textarea value={text} onChange={(e) => setText(e.target.value)} placeholder="开始打字以触发自动保存..." />
</div>
)
}

useUpdateEffect Hook 写法h4

import { useState, useEffect, useRef } from 'react'
function useUpdateEffect(effect, deps) {
const isMountedRef = useRef(false)
useEffect(() => {
if (!isMountedRef.current) {
isMountedRef.current = true
return
}
return effect()
}, deps)
}
function MyComponent() {
const [count, setCount] = useState(0)
// 只在 count 更新时打印日志,首次渲染时不打印
useUpdateEffect(() => {
console.log('Count updated:', count)
}, [count])
return (
<div>
<p>Count: {count}</p>
<button onClick={() => setCount(count + 1)}>Increment</button>
</div>
)
}

总结h2

useRef 的核心价值,不是“替代 state”,而是保存那些需要跨渲染保留、但又不该触发视图更新的数据。

如果只是操作 DOM,useRef 可以让你拿到元素实例;如果是处理定时器、异步回调、上一轮数据这类状态外信息,它又能提供一个稳定、可变的容器。把它用在合适的地方,代码会比“硬塞进 state”更自然,也更符合 React 的数据流设计。

评论