在 React 中,useRef 是一个看似简单却非常强大的 Hook。很多初学者会把它和 useState 混为一谈,认为它们都是"存储数据"的工具。但事实并非如此。
今天我们就通过两个实际例子,深入剖析 useRef 的真正作用:它不是一个状态容器,而是一个"跨渲染周期的引用容器"。
一、useRef 到底是用来干什么的?
我们先看一段代码:
javascript
import { useState, useRef, useEffect } from 'react';
export default function App() {
const [count, setCount] = useState(0);
const inputRef = useRef(null);
useEffect(() => {
inputRef.current?.focus();
}, []);
return (
<>
<input ref={inputRef} />
<button onClick={() => setCount(count + 1)}>
count++
</button>
<p>当前值: {count}</p>
</>
);
}
这段代码中,inputRef 被用来获取 <input> 元素的 DOM 引用,并在组件挂载后自动聚焦。
这里的关键是:
inputRef.current 在组件重新渲染时不会被重置。即使 count 改变导致组件重新渲染,inputRef 依然指向同一个 DOM 节点。
这正是 useRef 的核心能力:持久化引用,不随渲染而销毁。
二、useRef 和 useState 的本质区别
很多人会问:"既然都能存东西,为什么不直接用 useState?"
我们来对比一下:
| 特性 | useState | useRef |
|---|---|---|
| 是否触发重渲染 | 是 | 否 |
| 是否响应式 | 是 | 否 |
| 存储内容类型 | 状态数据(如用户输入、计数) | 任意值(DOM、定时器、函数等) |
| 使用场景 | 需要 UI 更新的数据 | 不需要 UI 更新的临时或持久引用 |
示例 1:用 useRef 存储定时器 ID
javascript
import { useState, useRef, useEffect } 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);
}
useEffect(() => {
console.log('当前 intervalId:', intervalId.current);
}, [count]);
return (
<>
<button onClick={start}>开始</button>
<button onClick={stop}>停止</button>
<p>Count: {count}</p>
<button onClick={() => setCount(count + 1)}>+1</button>
</>
);
}
在这个例子中:
intervalId.current保存了setInterval返回的 ID。- 即使组件因为
count变化而重新渲染,intervalId.current仍然保留着原来的值。 - 因此
stop()函数可以正确清除定时器。
如果你改用 useState 来存这个 ID,每次更新都会触发重渲染,造成不必要的性能损耗。
三、如果不使用 useRef 会发生什么?
我们来看一个错误版本:
javascript
import { useState, useEffect } from 'react';
export default function App() {
let intervalId = null;
const [count, setCount] = useState(0);
function start() {
intervalId = setInterval(() => {
console.log('tick~~');
}, 1000);
}
function stop() {
clearInterval(intervalId);
}
useEffect(() => {
console.log('当前 intervalId:', intervalId);
}, [count]);
return (
<>
<button onClick={start}>开始</button>
<button onClick={stop}>停止</button>
<p>Count: {count}</p>
<button onClick={() => setCount(count + 1)}>+1</button>
</>
);
}
问题分析:
- 点击「开始」→ 创建定时器,
intervalId被赋值为某个 ID(比如 12345)。 - 点击「+1」→ 组件重新渲染 →
let intervalId = null;执行 →intervalId变成null。 - 再点击「停止」→
clearInterval(null)→ 无效! - 定时器仍在运行,无法清除 → 内存泄漏!
这就是为什么不能用普通变量或 useState 来存储非状态数据。
四、useRef 的三大典型应用场景
- 获取 DOM 节点引用
ini
const inputRef = useRef(null);
<input ref={inputRef} />
用于操作 DOM,比如聚焦、滚动、获取值等。
- 存储可变对象(如定时器、WebSocket)
ini
const timerRef = useRef();
timerRef.current = setInterval(() => {}, 1000);
避免重复创建,确保能正确清理。
- 创建"持久化"的引用对象
ini
const prevValueRef = useRef();
function handleChange(value) {
console.log('上一次的值:', prevValueRef.current);
prevValueRef.current = value;
}
在闭包中保持对旧值的访问。
五、总结:useRef 是什么?
useRef 并不是"状态",而是 一个跨渲染周期的引用容器。
你可以把它想象成一个"保险箱":
- 组件反复重建,但保险箱一直存在;
- 你可以往里面放任何东西(DOM、ID、对象、函数);
- 它不会触发重渲染,也不会影响 UI;
- 但它能帮你记住那些"不该被遗忘"的东西。