一文搞懂 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 小时前
html框架页面介绍及制作
前端·html
be or not to be3 小时前
CSS 定位机制与图标字体
前端·css
DevUI团队4 小时前
🔥Angular高效开发秘籍:掌握这些新特性,项目交付速度翻倍
前端·typescript·angular.js
Moment4 小时前
如何在前端编辑器中实现像 Ctrl + Z 一样的撤销和重做
前端·javascript·面试
宠..4 小时前
优化文件结构
java·服务器·开发语言·前端·c++·qt
Tencent_TCB4 小时前
AI Coding全流程教程——0基础搭建“MEMO”健康打卡全栈Web应用(附提示词)
前端·人工智能·ai·ai编程·codebuddy·claude code·cloudbase
小猪猪屁4 小时前
权限封装不是写个指令那么简单:一次真实项目的反思
前端·javascript·vue.js
hteng4 小时前
跨域 Iframe 嵌套:调整内部 Iframe 高度的终极指南 (以及无解的真相)
前端
Polaris_o4 小时前
轻松上手Bootstrap框架
前端
1024小神4 小时前
微信小程序前端扫码动画效果绿色光效移动,四角椭圆
前端