在 React 的学习过程中,你一定遇到过这种困境:
- 我想操作 DOM(比如让输入框自动聚焦),但 React 告诉我不要直接操作 DOM。
- 我想保存一个变量,不希望它重置,但也不希望它的改变触发组件重新渲染。
- 我在
useEffect里怎么都拿不到最新的 State 值(闭包陷阱)。
这时候,你需要的就是 useRef。
一、 什么是 useRef?
简单来说,useRef 创建了一个普通的 JavaScript 对象,它长这样:
{
current: ... // 这里存着你的值
}
它有两个核心特性,必须死记硬背:
- 引用透传:在组件的整个生命周期内,这个对象永远是同一个(引用地址不变)。
- 变更不渲染:修改
ref.current的值,不会触发组件重新渲染(这与useState完全相反)。
打个比方:
useState像是橱窗里的模特。换了衣服(状态改变),大家都能看见(页面重绘)。useRef像是你口袋里的记事本。你写了什么(修改值),只有你自己知道,外面的人看不见(页面不会重绘)。
二、 场景一:访问 DOM 节点(最常见用法)
React 是声明式的,我们通常不需要直接碰触 DOM。但在某些场景下(管理焦点、文本选择、媒体播放、强制动画等),我们需要“逃生舱”。
import { useRef, useEffect } from 'react';
export default function TextInputWithFocusButton() {
// 1. 创建一个 ref,初始值为 null
const inputEl = useRef(null);
const onButtonClick = () => {
// 3. 通过 .current 访问真实的 DOM 节点
// 注意:React 会在组件挂载后,自动把 DOM 赋给 current
inputEl.current.focus();
console.log('输入框现在的宽度是:', inputEl.current.offsetWidth);
};
return (
<div>
{/* 2. 把 ref 绑定到 JSX 元素上 */}
<input ref={inputEl} type="text" />
<button onClick={onButtonClick}>点我聚焦输入框</button>
</div>
);
}
注意: 不要过度使用 Ref 操作 DOM。如果你发现自己在用 Ref 去修改 DOM 的内容(如 inputEl.current.value = 'hello'),这通常意味着你写错了,应该用 useState 控制。
三、 场景二:存储“幕后”变量(解决闭包陷阱)
还记得上一篇文章里的“闭包陷阱”吗?定时器里永远只能读到旧的 count。
除了把 count 加入依赖项,useRef 提供了一种更巧妙的解法:“逃课”大法。
既然闭包锁住的是变量的引用,那我们就创建一个永远不变的容器(Ref) ,把最新的值随时放进去。
import { useState, useEffect, useRef } from 'react';
export default function Timer() {
const [count, setCount] = useState(0);
// 1. 创建一个 Ref 用来“偷运”最新的 count
const latestCountRef = useRef(count);
// 2. 每次渲染,都把最新的 count 写入 Ref
// 这不会触发重绘,因为修改 Ref 是副作用
latestCountRef.current = count;
useEffect(() => {
const timer = setInterval(() => {
// 3. 定时器里不读 State,而是读 Ref
// 因为 Ref 对象的引用地址从未改变,所以闭包能一直访问到它
// 而 .current 属性总是被我们要么更新为最新的
console.log('定时器读取到的最新值:', latestCountRef.current);
}, 1000);
return () => clearInterval(timer);
}, []); // ✅ 依赖项可以为空!因为 Ref 对象本身是稳定的
return (
<div>
<p>Count: {count}</p>
<button onClick={() => setCount(count + 1)}>+1</button>
</div>
);
}
原理解析:
count是每次渲染都不同的数字(值类型)。闭包一旦形成,捕获的是当年的那个数字。latestCountRef是一个对象(引用类型)。闭包捕获的是这个对象的地址。哪怕里面的.current变了,只要地址没变,闭包就能顺藤摸瓜找到最新的值。
四、 场景三:记录“上一次”的值
有时候我们需要知道状态“上一次”是什么,比如判断股票是涨了还是跌了。React 没有提供 usePrevious 这样的 Hook,我们可以用 useRef 自己造一个。
import { useState, useEffect, useRef } from 'react';
function Counter() {
const [count, setCount] = useState(0);
// 用于存储上一次的值
const prevCountRef = useRef();
useEffect(() => {
// 渲染完成后,更新 ref
// 只有在下一次渲染时,我们才能拿出来对比
prevCountRef.current = count;
}); // 每次渲染后都执行
// 在本次渲染中,prevCountRef.current 还是旧值
// 因为 useEffect 是在渲染结构提交到屏幕**之后**才运行的
const prevCount = prevCountRef.current;
return (
<div>
<h1>当前: {count}</h1>
<h2>上一次: {prevCount}</h2>
<button onClick={() => setCount(count + 1)}>+1</button>
</div>
);
}
五、 灵魂拷问:为什么不用普通变量?
新手常问: “为什么不用 let variable = 0 定义在组件外面或者里面,非要用 useRef?”
1. 为什么不能定义在组件里面?
function App() {
let timerId = 0; // ❌ 错误
// ...
}
原因: 组件每次重新渲染,函数就会重新执行。timerId 会被重置为 0。你辛辛苦苦存的数据瞬间丢失。
2. 为什么不能定义在组件外面?
let timerId = 0; // ❌ 错误(除非是单例)
function App() {
// ...
}
原因: 如果你的页面上有 5 个 组件,它们会共享同一个全局的 timerId。一个组件改了,别的组件全乱套了。
useRef 保证了数据是“也就是组件实例独享的”,且“穿越渲染周期而不丢失”。
总结:useRef vs useState
| 特性 | useState | useRef |
|---|---|---|
| 主要用途 | 存储直接影响视图的数据 | 存储 DOM 引用、定时器 ID、无关视图的数据 |
| 数据变化时 | 触发组件重新渲染 | 不触发重新渲染 |
| 读取时机 | 渲染过程中直接读取 | 通常在 useEffect 或事件处理函数中读取 |
| 心智模型 | 组件的状态(State) | 组件的实例变量(Instance Variable) |
当你下一次想在 React 里存点东西,但又不想因为它变了而导致页面莫名其妙闪烁(重绘)时,请立刻想起 useRef!