Archive Note
React Hook 学习 3:useRef 的稳定引用与非渲染状态
useRef 经常被一句话概括成“拿 DOM 节点”,但这只是它的一部分用途。
更完整的理解应该是:
useRef提供了一个跨渲染稳定存在、可变但不会触发重新渲染的容器。
这句话很关键,因为它区分了 ref 和 state:
state变化会重新渲染ref变化不会重新渲染
因此 useRef 最适合处理那些需要记住,但不该驱动 UI 重绘的值。
一、原理:ref 是稳定对象,不是当前值变量
const inputRef = useRef<HTMLInputElement | null>(null);
React 返回给你的不是 DOM 本身,而是一个稳定对象:
{ current: ... }
这个对象在整个组件生命周期里保持同一个引用,只有 .current 会变化。
这也是为什么你可以用它保存:
- DOM 节点
- 定时器 id
- 第三方实例
- 上一次的值
- 最新回调引用
二、开发场景 1:操作 DOM
这是最常见的 use case。
const inputRef = useRef<HTMLInputElement | null>(null);
function focusInput() {
inputRef.current?.focus();
}
return <input ref={inputRef} />;
这种情况适合 useRef,因为你不是想让 UI 重新渲染,而是想在某个时机直接操作真实 DOM。
常见场景包括:
- input 自动聚焦
- 滚动到某个位置
- 读取元素尺寸
- 播放/暂停视频
三、开发场景 2:保存跨渲染值,但不触发重绘
比如保存定时器 id:
const timerRef = useRef<number | null>(null);
useEffect(() => {
timerRef.current = window.setInterval(() => {
console.log("tick");
}, 1000);
return () => {
if (timerRef.current !== null) {
clearInterval(timerRef.current);
}
};
}, []);
如果把这个 timer id 放进 state,不但没有意义,还会制造额外渲染。
四、开发场景 3:解决“最新值”问题
React 里的闭包问题,本质上是函数捕获了旧渲染时的值。
有些场景里,你希望异步逻辑总能拿到“最新值”,但又不想因为这个值变化而重新注册订阅或定时器。这时 useRef 很有用。
const latestKeywordRef = useRef(keyword);
useEffect(() => {
latestKeywordRef.current = keyword;
}, [keyword]);
之后某些异步回调里就可以读:
latestKeywordRef.current
这不是为了绕过 React,而是为了区分两件事:
- 哪些值应该驱动渲染
- 哪些值只是让异步逻辑读到最新信息
五、最常见误区:把 ref 当成 state 替代品
很多人学到 ref 不会触发重渲染后,会觉得“那我把所有状态都放 ref,不就更高效了吗?”
这是错误的。
比如:
const countRef = useRef(0);
countRef.current += 1;
如果你把 UI 也显示这个值:
return <div>{countRef.current}</div>;
你会发现页面不会自动更新。因为 ref 根本不是用来驱动 UI 的。
所以判断标准很简单:
- 值变化后要更新界面:用
state - 值变化后只是内部逻辑要记住:用
ref
六、和 useEffect 搭配时特别有用
useRef 经常是 useEffect 的配角。
例如:
- 保存订阅实例
- 保存第三方对象
- 保存最新回调
- 避免 effect 里反复初始化昂贵对象
一个典型例子是播放器实例:
const playerRef = useRef<Player | null>(null);
useEffect(() => {
playerRef.current = createPlayer(containerElement);
return () => {
playerRef.current?.destroy();
playerRef.current = null;
};
}, []);
七、不要在 render 阶段随意读写 ref
技术上你能在组件函数里直接改 .current,但大多数情况下这不是好习惯。
例如:
ref.current = someValue;
如果这是 render 期间的核心逻辑,很容易让组件行为变得不可预测。
更推荐的方式是:
- DOM 场景:交给
ref绑定 - 异步/副作用场景:在
useEffect或事件处理函数里写 - 需要驱动 UI:回到
state
八、真实开发建议
1. ref 最好带明确语义命名
不要总叫 ref。
例如:
inputReftimerReflatestValueRefchartInstanceRef
名字越清楚,越能提醒你它存的是什么。
2. ref 很适合存“外部对象句柄”
比如:
- timeout / interval id
- WebSocket 实例
- IntersectionObserver
- 第三方图表实例
这些东西都不该进 state。
3. 别把 ref 当成逃避依赖管理的工具
有些人会为了少写 effect 依赖,疯狂把东西塞进 ref。这通常是在掩盖设计问题,而不是解决问题。
ref 该用,但它是为了表达“这个值不属于渲染状态”,不是为了逃避 React 的数据流。
九、这一篇最该记住的结论
useRef保存的是跨渲染稳定存在的可变容器- ref 改变不会触发重新渲染
- 适合保存 DOM、实例、定时器、最新值等非渲染状态
- 不要把 ref 当成 state 替代品
- 当你想区分“UI 状态”和“内部记忆”时,通常就该想到
useRef
理解了 useRef,你会更清楚 React 组件里哪些东西属于“渲染系统”,哪些只是“运行时辅助数据”。