一文搞懂 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;
  • 但它能帮你记住那些"不该被遗忘"的东西。
相关推荐
崔庆才丨静觅1 小时前
hCaptcha 验证码图像识别 API 对接教程
前端
passerby60612 小时前
完成前端时间处理的另一块版图
前端·github·web components
掘了2 小时前
「2025 年终总结」在所有失去的人中,我最怀念我自己
前端·后端·年终总结
崔庆才丨静觅2 小时前
实用免费的 Short URL 短链接 API 对接说明
前端
崔庆才丨静觅3 小时前
5分钟快速搭建 AI 平台并用它赚钱!
前端
崔庆才丨静觅3 小时前
比官方便宜一半以上!Midjourney API 申请及使用
前端
Moment3 小时前
富文本编辑器在 AI 时代为什么这么受欢迎
前端·javascript·后端
崔庆才丨静觅3 小时前
刷屏全网的“nano-banana”API接入指南!0.1元/张量产高清创意图,开发者必藏
前端
剪刀石头布啊3 小时前
jwt介绍
前端
爱敲代码的小鱼4 小时前
AJAX(异步交互的技术来实现从服务端中获取数据):
前端·javascript·ajax