在 React 函数组件的世界中,useRef 是一个常被提及但又容易被误解的 Hook。很多初学者第一次接触它时,往往只把它当作"获取 DOM 元素"的工具;而随着使用深入,又可能会疑惑:为什么不能用 useRef 替代 useState 来避免不必要的重渲染?本文将结合两个典型示例,系统梳理 useRef 的核心机制、使用场景与常见误区,帮助你真正掌握这个"默默奉献"的 Hook。
一、useRef 的基本能力:持久化引用对象
useRef 的本质是创建一个可变且持久化的引用对象 。它的返回值是一个普通 JavaScript 对象,结构为 { current: initialValue }。关键在于:
- 每次组件重新渲染时,
useRef返回的是同一个对象引用; - 修改
.current属性不会触发组件重新渲染; - 它不参与 React 的响应式更新机制,因此被称为"非响应式存储"。
这与 useState 形成鲜明对比:useState 的状态变更会触发 UI 更新,而 useRef 的变更则"静默"发生。
二、场景一:获取 DOM 元素并操作
最常见的 useRef 用法是绑定到 JSX 元素上,从而在组件逻辑中直接操作 DOM。
javascript
import { useRef, useEffect, useState } from 'react';
export default function App() {
const [count, setCount] = useState(0);
const inputRef = useRef(null);
useEffect(() => {
// 组件挂载后,inputRef.current 指向真实的 <input> 元素
inputRef.current.focus();
}, []);
return (
<>
<input ref={inputRef} />
{count}
<button onClick={() => setCount(count + 1)}>count ++</button>
</>
);
}
在这个例子中:
- 初始渲染时,
inputRef.current为null,因为 DOM 尚未生成; - React 在完成 DOM 挂载后,会自动将对应的 DOM 节点赋值给
ref.current; useEffect(依赖项为空数组)在首次挂载后执行,此时inputRef.current已是有效的<input>元素,调用.focus()实现自动聚焦。
值得注意的是:即使 inputRef.current 从 null 变为 DOM 节点,组件也不会重新渲染 。这正是 useRef 的设计初衷------提供一种不干扰 React 渲染流程的方式来访问或存储数据。
三、场景二:存储可变值以避免状态重置
除了操作 DOM,useRef 还非常适合用于在多次渲染之间持久化存储可变值,尤其是在处理副作用(如定时器)时。
考虑以下错误写法:
javascript
// ❌ 错误:使用普通变量存储定时器 ID
let intervalId = null;
function start() {
intervalId = setInterval(() => {
console.log('tick~~~');
}, 1000);
}
function stop() {
clearInterval(intervalId); // 可能为 null!
}
问题在于:每当 count 状态更新,整个函数组件会重新执行,let intervalId = null 会被再次初始化,导致之前保存的定时器 ID 丢失。结果是:
- 多次点击"开始"会创建多个定时器;
- "停止"按钮无法清除旧的定时器;
- 造成内存泄漏 和逻辑混乱。
正确的做法是使用 useRef:
javascript
import { useEffect, useRef, useState } from 'react';
export default function App() {
const intervalId = useRef(null);
const [count, setCount] = useState(0);
function start() {
intervalId.current = setInterval(() => {
console.log('tick~~~');
}, 1000);
}
function stop() {
clearInterval(intervalId.current);
}
return (
<>
<button onClick={start}>开始</button>
<button onClick={stop}>停止</button>
{count}
<button onClick={() => setCount(count + 1)}>count ++</button>
</>
);
}
这里的关键在于:
useRef(null)在组件整个生命周期内始终返回同一个对象;- 即使
count更新导致组件重新渲染,intervalId.current依然保留着上次设置的定时器 ID; - 因此
clearInterval能正确清除目标定时器,避免资源泄露。
四、useRef 与 useState 的核心区别
虽然 useRef 和 useState 都能"存值",但它们的设计目标截然不同:
| 特性 | useState |
useRef |
|---|---|---|
| 是否触发重渲染 | ✅ 是 | ❌ 否 |
| 值变更是否被 React 跟踪 | ✅ 是(通过调度更新) | ❌ 否 |
| 适用场景 | 驱动 UI 变化的状态 | 存储不需触发 UI 更新的可变数据 |
| 初始值 | 每次调用 useState(initial) 仅在首次生效 |
useRef(initial) 的初始值也仅在首次生效,但后续 .current 可任意修改 |
常见误区:能否用 useRef 替代 useState?
不能。
假设你试图用 useRef 存储计数器值以"避免重渲染":
javascript
const countRef = useRef(0);
// ...
<button onClick={() => countRef.current++}>+1</button>
<p>{countRef.current}</p>
你会发现:点击按钮后,页面上的数字不会更新 !因为 React 并不知道 countRef.current 发生了变化,自然不会重新执行渲染逻辑。
结论:
- 如果你需要同时存储值并更新 UI ,必须使用
useState; - 如果你只需要在不触发重渲染的前提下保存中间状态或引用 (如定时器 ID、前一次的 props、滚动位置等),才应使用
useRef。
五、useRef 的典型应用场景总结
-
访问 DOM 节点
如聚焦输入框、测量元素尺寸、触发动画等。
-
持久化存储可变值
- 定时器/延时器 ID(
setInterval/setTimeout) - WebSocket 实例
- 第三方库的实例(如地图、图表对象)
- 上一次的 props 或 state(用于对比)
- 定时器/延时器 ID(
-
跨渲染保持状态而不触发更新
例如记录组件是否已挂载(在异步回调中判断是否还能安全 setState)。
结语
useRef 虽然名字里有 "ref",但它远不止是"获取 DOM 的工具"。它是一个轻量级、非响应式的持久化容器,在需要"记住某些东西但又不想打扰 React 渲染流程"时大显身手。
正确使用 useRef,能让你的组件更高效、更健壮;而误用它(如试图替代状态管理),则会导致 UI 不更新或逻辑错乱。理解其"非响应式"和"引用持久化"的两大特性,是掌握这一 Hook 的关键。
在实际开发中,当你遇到以下情况时,不妨想想 useRef:
- "我需要在组件里保存一个值,但它变了不需要刷新页面。"
- "我想在挂载后操作某个 DOM 元素。"
- "我的定时器怎么关不掉了?是不是 ID 丢了?"
答案,往往就在 useRef 之中。