← Back to desk

Archive Note

React Hook 学习 3:useRef 的稳定引用与非渲染状态

Filed As react-hook-useref

useRef 经常被一句话概括成“拿 DOM 节点”,但这只是它的一部分用途。

更完整的理解应该是:

useRef 提供了一个跨渲染稳定存在、可变但不会触发重新渲染的容器。

这句话很关键,因为它区分了 refstate

  • 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

例如:

  • inputRef
  • timerRef
  • latestValueRef
  • chartInstanceRef

名字越清楚,越能提醒你它存的是什么。

2. ref 很适合存“外部对象句柄”

比如:

  • timeout / interval id
  • WebSocket 实例
  • IntersectionObserver
  • 第三方图表实例

这些东西都不该进 state。

3. 别把 ref 当成逃避依赖管理的工具

有些人会为了少写 effect 依赖,疯狂把东西塞进 ref。这通常是在掩盖设计问题,而不是解决问题。

ref 该用,但它是为了表达“这个值不属于渲染状态”,不是为了逃避 React 的数据流。

九、这一篇最该记住的结论

  • useRef 保存的是跨渲染稳定存在的可变容器
  • ref 改变不会触发重新渲染
  • 适合保存 DOM、实例、定时器、最新值等非渲染状态
  • 不要把 ref 当成 state 替代品
  • 当你想区分“UI 状态”和“内部记忆”时,通常就该想到 useRef

理解了 useRef,你会更清楚 React 组件里哪些东西属于“渲染系统”,哪些只是“运行时辅助数据”。