一文搞懂 useRef:它到底在“存”什么?

在 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>
    </>
  );
}

问题分析:

  1. 点击「开始」→ 创建定时器,intervalId 被赋值为某个 ID(比如 12345)。
  2. 点击「+1」→ 组件重新渲染 → let intervalId = null; 执行 → intervalId 变成 null
  3. 再点击「停止」→ clearInterval(null) → 无效!
  4. 定时器仍在运行,无法清除 → 内存泄漏!

这就是为什么不能用普通变量或 useState 来存储非状态数据。


四、useRef 的三大典型应用场景

  1. 获取 DOM 节点引用
ini 复制代码
const inputRef = useRef(null);
<input ref={inputRef} />

用于操作 DOM,比如聚焦、滚动、获取值等。

  1. 存储可变对象(如定时器、WebSocket)
ini 复制代码
const timerRef = useRef();
timerRef.current = setInterval(() => {}, 1000);

避免重复创建,确保能正确清理。

  1. 创建"持久化"的引用对象
ini 复制代码
const prevValueRef = useRef();

function handleChange(value) {
  console.log('上一次的值:', prevValueRef.current);
  prevValueRef.current = value;
}

在闭包中保持对旧值的访问。


五、总结:useRef 是什么?

useRef 并不是"状态",而是 一个跨渲染周期的引用容器。

你可以把它想象成一个"保险箱":

  • 组件反复重建,但保险箱一直存在;
  • 你可以往里面放任何东西(DOM、ID、对象、函数);
  • 它不会触发重渲染,也不会影响 UI;
  • 但它能帮你记住那些"不该被遗忘"的东西。
相关推荐
代码搬运媛3 小时前
Jest 测试框架详解与实现指南
前端
counterxing4 小时前
我把 Codex 里的 Skills 做成了一个 MCP,还支持分享
前端·agent·ai编程
wangqiaowq4 小时前
windows下nginx的安装
linux·服务器·前端
之歆5 小时前
DAY_12JavaScript DOM 完全指南(二):实战与性能篇
开发语言·前端·javascript·ecmascript
发现一只大呆瓜5 小时前
Vite凭什么这么快?3分钟带你彻底搞懂 Vite 热更新的幕后黑手
前端·面试·vite
Maimai108085 小时前
React如何用 @microsoft/fetch-event-source 落地 SSE:比原生 EventSource 更灵活的实时推送方案
前端·javascript·react.js·microsoft·前端框架·reactjs·webassembly
kyriewen6 小时前
产品经理把PRD写成“天书”,我用AI半小时重写了一遍,他当场愣住
前端·ai编程·cursor
humcomm7 小时前
元框架的工作原理详解
前端·前端框架
canonical_entropy7 小时前
Attractor Before Harness: AI 大规模开发的方法论
前端·aigc·ai编程
zhangxingchao8 小时前
多 Agent 架构到底怎么选?从 Claude Agent Teams、Cognition/Devin 到工程落地原则
前端·人工智能·后端