上来就用 useState?你大概率用错了——useRef 的三种正确打开方式

useState 存个计数器,页面怎么不动?彻底搞懂 useRef


你写了一个计数器:

jsx 复制代码
function Counter() {
  const [count, setCount] = useState(0);

  const add = () => {
    count += 1;           // 改了 count
    console.log(count);   // 打印出来确实是 1
  };

  return <div onClick={add}>{count}</div>;
}

点一下,控制台打印 1。但页面上------纹丝不动,还是 0

你愣了一下,改成 setCount(count + 1)。好了,页面动了。

但过两天你又遇到一个新场景:需要在组件里持有一个 Web Worker 实例,要给 worker 发消息。你用 useState 存了它:

jsx 复制代码
const [worker, setWorker] = useState(null);

useEffect(() => {
  const w = new Worker(new URL('./worker.js', import.meta.url));
  setWorker(w);          // ⚠️ 触发了一次不必要的渲染
}, []);

页面正常工作。但你觉得哪里不太对------我就存个 worker 引用,为什么还要让 React 重新跑一遍渲染流程?

这篇文章就是解决这个困惑的:useRef 到底是什么,什么时候该用它而不是 useState。


一句话,useRef 到底是什么

useRef 是 React 提供的「非响应式可变盒子」------它能在多次渲染之间持久保存一个值,但修改它不会触发组件重新渲染。

拆开来看:

  • 持久:值从组件挂载到卸载一直存在,不会因为渲染而被重置
  • 可变 :你可以直接修改 ref.current,不需要 setter 函数
  • 非响应式 :改了不会触发渲染------这是它和 useState 最根本的区别

用一个生活场景来理解:

csharp 复制代码
useState 就像你的银行账户余额------每次变动银行都会给你发短信通知(触发渲染),
你能在 App 上看到最新数字。

useRef 就像你口袋里的一张便签纸------你可以随时在上面改数字,
但没人会主动通知你更新了。只有你自己低头看(手动读 ref.current)才知道。

这个区别决定了两种完全不同的使用场景。


场景一:绑定 DOM 节点(你最熟悉的用法)

页面加载后,输入框自动获得焦点------这是 useRef 最经典的场景:

jsx 复制代码
import { useRef, useEffect } from 'react';

function App() {
  const inputRef = useRef(null); // 🔑 初始为 null,后面会指向真实 DOM

  useEffect(() => {
    console.log(inputRef.current); // 挂载后,current 已经指向 input 元素
    inputRef.current.focus();      // 直接调用原生 DOM API
  }, []);

  return <input type="text" ref={inputRef} placeholder="请输入用户名" />;
}

运行结果:

lua 复制代码
<input type="text" placeholder="请输入用户名">   // console.log 输出
(页面加载后输入框自动聚焦)

这里 ref={inputRef} 做的事情非常简单:React 在创建这个 <input> 的真实 DOM 节点后,把它的引用塞进 inputRef.current

为什么不能用 useState 存 DOM?

jsx 复制代码
// ❌ 错误思路
const [inputEl, setInputEl] = useState(null);

// 你打算怎么拿到 DOM 节点?用 ref 回调?
// <input ref={(el) => setInputEl(el)} />
// 这确实能拿到------但会引起双重渲染:
// 第一次渲染:inputEl = null
// setInputEl(domNode) 触发第二次渲染:inputEl = domNode  ← 完全没必要

DOM 节点是一个"外部对象",它不属于 React 的数据流。useState 存它,等于强加了一层 React 的响应式开销,而这份开销毫无意义------DOM 节点变了不需要重新渲染,因为 React 已经知道了。


场景二:存储不触发渲染的可变值

看这个计数器:

jsx 复制代码
function App() {
  const numRef = useRef(0); // 🔑 可变对象,初始值 0
  const [, forceRender] = useState(0); // ⚠️ 只是一个"手动触发渲染"的手段

  console.log(numRef.current); // 每次渲染都会打印当前值

  return (
    <div onClick={() => {
      numRef.current += 1;   // 直接改 current,不触发渲染
      forceRender(x => x + 1); // 手动强制渲染,让你看到变化
    }}>
      {numRef.current}
    </div>
  );
}

等等------既然改了 numRef.current 不触发渲染,那我怎么让用户看到变化?上面用了一个取巧的办法:forceRender。但这只是 Demo 用的展示技巧。

真正的区别在这里

useRefuseState 放在一起对比,差异一目了然:

useState useRef
读取方式 count ref.current
修改方式 setCount(newValue) ref.current = newValue
修改后触发渲染? ✅ 是 ❌ 否
渲染期间值是否稳定? ✅ 是(同一轮渲染内不变) ⚠️ 否(随时可变)
典型用途 UI 数据、表单输入、开关状态 DOM 引用、定时器 ID、Worker 实例

关键认知:useRef 不是为了"省掉一次渲染"而存在的。 它的设计目的是让你持有那些本质上不属于 UI 状态的东西------改了不需要更新界面,但需要在多次渲染间保持同一个引用。

那什么东西"本质上不属于 UI 状态"?看第三个场景。


场景三:持有「外部对象」------最被低估的用法

你的页面需要做一个复杂计算(比如 LLM 推理、图像处理、大量数据排序),放在主线程会卡死界面。于是你打算开一个 Web Worker:

graph LR A[&#34;主线程 ─ React 组件&#34;] -->|&#34;new Worker()&#34;| B[&#34;Worker 线程&#34;] B -->|&#34;postMessage&#34;| C[&#34;复杂计算&#34;] C -->|&#34;postMessage 返回结果&#34;| A A -->|&#34;setState&#34;| D[&#34;更新 UI&#34;]

最简单的实现:

jsx 复制代码
import { useRef, useEffect } from 'react';

function App() {
  const workerRef = useRef(null); // 🔑 持久化 Worker 引用,但不触发渲染

  useEffect(() => {
    // 开启 Worker 线程------开销较大,只需要做一次
    workerRef.current = new Worker(
      new URL('./worker.js', import.meta.url)
    );

    // ⚠️ 易错:组件卸载时必须清理 Worker,否则内存泄漏
    return () => {
      workerRef.current?.terminate();
    };
  }, []);

  return <>{/* Worker 在后台运行,不占用 UI 状态 */}</>;
}

为什么 Worker 必须用 useRef 而不能用 useState

原因有三层,一层比一层深:

第一层------语义层:Worker 不属于 UI 状态。你的界面不需要根据 "Worker 是哪个实例" 来重新渲染。Worker 是基础设施,不是数据。

第二层------性能层 :用 useState 存 Worker 会触发一次无意义的渲染。虽然一次无所谓,但这是代码腐化的开始------每个"无所谓"最终堆成一座屎山。

第三层------正确性层 :这是最容易被忽略的关键点。假设你用 useState 存 Worker,然后在某个事件处理函数里发消息:

jsx 复制代码
// ❌ 用 useState 存 Worker 的隐患
const [worker, setWorker] = useState(null);

useEffect(() => {
  const w = new Worker(new URL('./worker.js', import.meta.url));
  setWorker(w);
}, []);

const handleClick = () => {
  worker.postMessage('hello'); // ⚠️ 如果这是组件首次渲染时的闭包,
                                // worker 还是 null!
};

React 的每次渲染都有自己的 props、state 和事件处理函数。如果你在首次渲染的闭包里捕获了 worker(当时是 null),后面 setWorker 更新了,但那个旧的闭包里的 worker 仍然是 null

useRef 解决了这个问题:ref 对象的引用在组件整个生命周期中不变 ,你始终通过 workerRef.current 访问最新值,不会出现闭包陷阱。

useRef 给你的不是一个"快照",而是一个"指针"。快照会过时,指针永远指向最新值。


回头再看一眼:那行被注释掉的代码

在 Demo 代码里,有一段被注释掉的 for 循环:

javascript 复制代码
// 被注释掉的主线程阻塞代码
// for(let i = 0; i < 100000000; i++) {
//   console.log(i)
// }

这段代码如果取消注释,会发生什么?浏览器卡死------1 亿次循环把主线程占满,在这期间所有用户交互(点击、滚动、输入)全部无响应。

这就是为什么要用 Worker + useRef 的组合:

  • Worker 解决了「主线程阻塞」的问题------耗时计算移到后台
  • useRef 解决了「如何持有 Worker 引用」的问题------一个稳定的、不自作主张的指针

两者配合,React 组件专注于 UI 渲染,Worker 专注于计算,useRef 负责在两者之间建一座桥梁。


总结:一张表理清 useState vs useRef

csharp 复制代码
你有一个值,需要在组件里存着。该用什么?

值变了,界面要更新吗?
├── 要 → useState
└── 不要
    ├── 是 DOM 节点引用? → useRef + ref 属性
    ├── 是 Worker / 定时器 ID / WebSocket / AbortController? → useRef
    └── 是普通变量,但不属于 UI 状态? → useRef

用一句话记住:useRef 是 React 响应式世界里的一块 "非响应式飞地"------你可以在里面存任何东西,改了 React 不知道,React 也不应该知道。


下次写代码,你可以这样做

  • 创建新 state 之前,问自己一句:「这个值变了,真的需要重新渲染吗?」
  • 如果答案是「不用」,别犹豫------上 useRef
  • 遇到 useState(null) 后面用 ref 回调或 useEffect 往里塞 DOM 节点/外部对象的写法,立即意识到这是反模式

开放问题 :你在项目中遇到过因为误用 useState 存不该响应式的值而导致的 bug 吗?比如定时器 ID、WebSocket 连接、或者其他场景?评论区聊聊。


相关推荐
慧一居士1 小时前
Naive UI vs Element Plus全面对比
前端框架
光影少年2 小时前
RN的Fabric 渲染流程
运维·前端·javascript·react native·react.js·fabric
烬羽3 小时前
《React Router 受保护路由的 3 个坑,第 2 个 90% 的人都踩过》
react.js·性能优化·全栈
__zRainy__3 小时前
React开始:直接在网站中使用React.js
前端·react.js·前端框架
用户938515635074 小时前
React Router 进阶:路由守卫、登录鉴权与状态传递
前端·javascript·全栈
触底反弹6 小时前
🚀 20 行代码手写前端路由 + React Router 核心 API 一篇搞定!
前端·javascript·react.js
小林ixn7 小时前
React Router 从入门到实战:一篇搞定路由配置、懒加载与嵌套路由
前端·react.js·前端框架
会飞的喵8 小时前
我给 AI Agent 做了个 UI 渲染 SDK:基于 Google A2UI 协议,让 Agent 不再只会发文字
前端框架·aigc
先吃饱再说8 小时前
从多页到单页:前端路由的演进与 React Router
前端·react.js·前端框架