useRef

一、先理解一个根本问题:组件里"普通变量"为什么存不住东西?

React 的函数组件,本质是一个普通函数 。每次渲染,React 都会重新调用这个函数一次。这意味着:

复制代码
function Timer() {
  let intervalId = null;   // ← 每次渲染都被重新创建
  // ...
}

函数体里声明的普通局部变量,每次渲染都从零开始------上一轮赋值进去的东西,下一轮就丢了。

那用 state(useState) 存行不行?可以存住,但有个代价:state 一变化就会触发整个组件重新渲染 。而我们想存的定时器 ID、前一次的值,往往根本不需要重新渲染 UI------存它是为了"记住",不是为了"显示"。

于是就有了第三种选择:useRef。


二、useRef 的本质:一个"不会丢、也不会触发渲染"的盒子

先看它的形态:

复制代码
const myRef = useRef(初始值);

useRef 返回一个对象 ,这个对象里只有一个属性 current,初始值就是传给它的参数:

复制代码
myRef = { current: 初始值 }

它的两个关键特性,正好同时解决上面两个问题:

特性 说明
跨渲染存活 React 在第一次渲染 创建这个对象,之后每次渲染返回给你的都是同一个对象 ,里面的 current 值不会被重置
改值不触发渲染 你直接改 myRef.current = xxx,这只是普通的属性赋值,React 不会因此重新渲染组件

一句话:useRef 是"能跨渲染存活、但又不会引发重渲染"的可变数据容器。 这正是"保存可变数据"这个用途的全部基础。

补充:useRef 参数里的初始值只在第一次渲染时用一次,之后 React 每次都给你同一个对象。


三、场景一:保存定时器 ID(setInterval 的句柄)

这个场景的痛点

setInterval 会返回一个 ID 号 ,你想停掉定时器,必须用这个 ID 调用 clearInterval(id)。所以这个 ID 必须在多次渲染之间活下来。

试想三种存法:

  1. 普通局部变量 → 每次渲染被重置,停止时 ID 已经丢了,不行;
  2. useState → 能存住,但每赋一次值就多触发一次渲染,浪费;
  3. useRef → 存得住、又不触发渲染,正合适。

完整代码 + 逐步讲解

TypeScript 复制代码
import { useRef } from 'react';

function Timer() {
  // ① 创建一个 ref 盒子,初始值为 null,专门存定时器 ID
  const timerRef = useRef(null);

  const start = () => {
    // ② 启动定时器,把返回的 ID 放进盒子里
    timerRef.current = setInterval(() => {
      console.log('Tick');
    }, 1000);
  };

  const stop = () => {
    // ③ 从盒子里取出 ID,用它停止定时器
    clearInterval(timerRef.current);
    timerRef.current = null;   // 清空,表示已停止
  };

  return (
    <>
      <button onClick={start}>开始</button>
      <button onClick={stop}>停止</button>
    </>
  );
}

逐步拆解:

  • 第 ② 步:timerRef.current = setInterval(...) ------ 把定时器 ID 放进盒子 。之后无论组件渲染多少次,这个 ID 都稳稳地待在 timerRef.current 里;
  • 第 ③ 步:clearInterval(timerRef.current) ------ 从盒子里取出 ID 来停止定时器;
  • 因为改 timerRef.current 不会触发重渲染,所以这个组件里点"开始/停止"不会白渲染。

进阶提醒(必看) :真实的定时器组件,一定要在组件卸载时清理定时器 ,否则会内存泄漏。通常配合 useEffect 的清理函数做:

复制代码
useEffect(() => {
  return () => clearInterval(timerRef.current);   // 组件卸载时停掉
}, []);

四、场景二:保存"前一次的值"(usePrevious)

这个场景的痛点

useState 只给你当前 的值。一旦触发重渲染,旧值就被 React 丢掉了------你拿不到"上一次是多少"。

但很多需求需要做前后对比,比如:

  • 检测某个值"从旧值变成了新值",从而触发动画;
  • 判断数据是否真的变化了,再决定要不要发请求。

这时就需要用 ref 把"上一次的值"额外记一份。

完整代码 + 逐步讲解(封装成自定义 Hook)

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

// 自定义 Hook:记住"上一次的值"
function usePrevious(value) {
  const ref = useRef();                 // ① 盒子,用来存旧值

  useEffect(() => {
    ref.current = value;                // ② 每次 value 变化后,把新值存进盒子
  }, [value]);

  return ref.current;                   // ③ 返回的,是"上一次"的值
}

使用:

复制代码
function Counter() {
  const [count, setCount] = useState(0);
  const prevCount = usePrevious(count);   // 上一次的 count

  return (
    <div>
      当前:{count}
      上一次:{prevCount ?? '无'}
      <button onClick={() => setCount(c => c + 1)}>+1</button>
    </div>
  );
}

这里最关键的是时序,请务必理解这个"先读后写"的顺序:

  1. 假设当前 count = 1,组件开始渲染;
  2. 渲染时执行 usePrevious(1),返回 ref.current ------ 但此刻 ref.current 里存的还是上一次的值 0(因为 useEffect 还没跑);
  3. 渲染完成之后 ,useEffect 才执行,把 ref.current = 1(这次的新值)存进去;
  4. 于是下一次渲染 时,usePrevious 返回的 ref.current 就正好是上一次的 0。

所以效果是:界面永远能同时看到"当前值"和"上一次的值" 。这就是标准的 usePrevious 模式,很多教程都这么写。

注意:useEffect 是渲染之后才跑的(React 提交 DOM 后执行副作用),这正是这个模式能"延迟一步记住旧值"的原因。


五、总结:什么时候用 ref 存可变数据?

用一个判断标准就能记牢:

这个数据需不需要"跨渲染存活",但又不需要"引起重渲染"?

  • 需要 → 用 useRef
  • 不需要(改它就该刷界面)→ 用 useState

两个场景对号入座:

场景 存什么 为什么要 ref(而不是变量/state)
定时器 setInterval 返回的 ID 变量存不住;state 会白触发渲染
前一次值 上一次的 state/props 借 useEffect 延迟一步,把旧值"留在盒子里"

共同本质 :useRef 就是给 React 函数组件用的"实例变量 "------相当于类组件里的 this.xxx 字段,存那些"要记住、但不上屏"的可变数据。


以上就是 8.2 这一节的完整文字讲解。如果你愿意,我还可以进一步:

  • 把两个场景合并到同一个真实小组件里演示;
  • 或者专门讲一下"为什么 usePrevious 一定要用 useEffect 而不是直接赋值"这个更深的点。

你想继续深入哪一块?

相关推荐
蓝悦无人机1 小时前
从“摸形状“到“点到线面的距离“——聊聊 SLAM 前端(激光雷达篇)
前端·loam·数据关联·激光slam·点云特征提取·点到线面距离·icp/ndt
米多科技1 小时前
我做了一个 3D 虚拟世界基底,在上面再搭 3D 世界或元宇宙,事半功倍
javascript·人工智能·github
flash俊杰1 小时前
工程约束怎么不拖后腿
前端
霍营_托尼2 小时前
纯前端播 40GB 本地视频?我把浏览器改造成了「磁盘流式」播放器
前端·javascript·音视频开发
溪语流沙2 小时前
【Web全栈进阶】FastAPI工程化:APIRouter拆分 + 配置 + 依赖注入
java·前端·fastapi
Shirley~~2 小时前
Three.js的基础概念
前端·3d
deli0072 小时前
黏菌没有大脑,3000 个 Physarum 粒子跑 12973 步自己铺出了迷宫最短路
前端
凌云若寒2 小时前
BarTender提示#807错误:无法在拥有其他许可证的Licensing Service上激活节点锁定的 Professional 版许可证 的解决办法
运维·服务器·前端·学习·软件需求
嘟嘟同学和妮妮同学2 小时前
用原生 JS 做了一个中国历史帝皇梳理的可视化站(7 朝 77 帝 + Leaflet 地图)
前端