R27第 1 / 1 课时未开始内容版本 1.0.0 · 核验于 2026-10-26

useRef 与可变引用

useRef 与可变引用 · 阅读后完成下方实验与检查题,再用「标记完成」记录进度。

学习目标

这一课给「计时器 id、上一次的值、DOM 节点」这类不参与渲染的数据找一个正经住处:useRef。读完你应该能判断一个值该放 state 还是 ref,并且不会再把界面需要的数字藏进 ref 里导致界面不动。

问题场景

写一个计数器,同事为了「少触发渲染」把计数放进了 ref:

function BadCounter() {
  const countRef = useRef(0);

  return (
    <button onClick={() => { countRef.current += 1; }}>
      点了 {countRef.current} 次
    </button>
  );
}

点十次,界面纹丝不动;随便再点一下别的按钮触发一次渲染,数字又「啪」地跳到 10。值明明在变,界面却像坏了一样。

核心解释

useRef(初始值) 返回一个跨渲染稳定存在的对象 { current: 初始值 }。组件卸载前,每次渲染拿到的都是同一个对象——这是它和普通变量的区别;但修改 ref.current 不会触发重新渲染——这是它和 state 的区别。

于是分工很清楚:

  • state 是给界面看的:值的变化要反映到界面上,用 state,React 负责触发渲染。
  • ref 是给逻辑用的:界面不需要知道它,只有事件处理和 Effect 需要读写,用 ref。

适合放 ref 的典型数据:setInterval 返回的计时器 id(暂停时拿来清除)、上一次渲染的某个值、DOM 节点引用、「是否已经发起过请求」的标记。它们都是控制逻辑的零件,不是界面内容。

反过来说,凡是 JSX 里要显示、要参与条件渲染的值,放 ref 就是在隐藏必要的更新——React 根本不知道它变了,自然不会重渲染。

示例拆解

正确的秒表:秒数用 state(界面要显示),计时器 id 用 ref(暂停时要清除):

function Stopwatch() {
  const [elapsed, setElapsed] = useState(0);
  const [running, setRunning] = useState(false);
  const timerRef = useRef(null);

  useEffect(() => {
    if (!running) return;
    timerRef.current = setInterval(() => {
      setElapsed((s) => s + 1); // 界面数据:state + 函数式更新
    }, 1000);
    return () => clearInterval(timerRef.current); // 计时器 id:ref
  }, [running]);

  return (
    <div>
      <p>已计时:{elapsed} 秒</p>
      <button onClick={() => setRunning((r) => !r)}>
        {running ? '暂停' : '开始'}
      </button>
    </div>
  );
}
  • elapsed 出现在 JSX 里,必须是 state,改动才会重新渲染。
  • timerRef.current 只在清理时被 clearInterval 用到,界面从不显示它,ref 正合适。
  • 顺手遵守了 R30 会展开的规矩:setElapsed(s => s + 1) 用函数式更新,回调里不依赖旧快照。

动手任务

打开本课实验区:

  1. 先连点十次「ref +1」按钮,观察界面不变;再点一次「state +1」触发渲染,看 ref 计数突然跳出来——体会「藏更新」的后果。
  2. 给秒表加一个「重置」按钮:停表并把 elapsed 归零,注意重置时也要清掉进行中的 interval。
  3. 把 timerRef 换成 useState 存计时器 id,功能不变——然后想一下:这里用 ref 好在哪?(提示:id 变化不需要渲染。)

常见错误

  • 界面数据放 ref:症状是「点了没反应,下次随便什么操作后数字乱跳」。原因就是改 ref.current 不触发渲染。
  • 在渲染期间读写 ref:组件函数体里直接 ref.current = xxx 或读它做计算,会让多次渲染结果不一致;ref 的读写应发生在事件处理和 Effect 里。
  • 指望改 ref 触发 Effect:useEffect(() => {...}, [myRef]) 的依赖是 ref 对象本身,每次渲染都一样;想监听变化请用 state。
  • 把 ref 当「绕过渲染的性能工具」:渲染慢应该用 useMemo、拆组件解决,用 ref 藏状态只是把 bug 推迟。

检查与复盘

完成下方检查题确认理解。自查清单:

  • 一句话说清分工:界面要跟着变的用 state,纯逻辑用的用 ref
  • 知道 ref 对象跨渲染稳定、改 current 不触发渲染这两条性质
  • 写代码前会问一句:这个值 JSX 里用不用?用,就是 state

参考资料

动手实验
动手实验
预览
点击「运行」在隔离沙箱中预览
检查题共 2 题,提交后立即显示解析
答案由服务端判定;答错的题会自动进入复习计划。
Q1单选简单

关于 useRef,下列说法正确的是?

Q2读代码中等

阅读代码(React 18+)。点击「开始」之后过一段时间再点「停止」,秒表能停下来吗?

function Timer() {
  const [seconds, setSeconds] = useState(0);
  const timerRef = useRef(null);

  function start() {
    timerRef.current = setInterval(() => {
      setSeconds((s) => s + 1);
    }, 1000);
  }

  function stop() {
    clearInterval(timerRef.current);
  }

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

读完这一课了?

标记完成后,工作台会安排接下来的学习。

Command Palette

Search for a command to run...