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

useState 与状态快照

useState 与状态快照 · 阅读后完成下方实验与检查题,再用「标记完成」记录进度。

学习目标

这一课解决一个几乎每个 React 学习者都会踩的坑:连续调用两次 setCount(count + 1),界面却只加了一次。读完你应该能解释为什么会这样,并且知道什么时候必须换成函数式更新。

问题场景

想象你在写一个学习计时器,按钮「加 5 分钟」的实现是一次性加 5。第一版代码是循环调用五次 setMinutes(minutes + 1):

function addFiveBroken() {
  for (let i = 0; i < 5; i++) {
    setMinutes(minutes + 1); // 结果:只加了 1
  }
}

点完按钮,分钟数从 0 变成了 1,不是 5。逻辑看起来没问题,结果却不对——问题不在循环,而在于 minutes 这个变量在这次函数执行期间是「旧」的。

核心解释

要理解这个现象,先接受一个模型:状态是快照,不是实时变量。

一次渲染(render)就是 React 调用你的组件函数一次。调用时传入的 minutes 是那次渲染时的状态值,好比一张照片。在这次函数执行的全程里,这张照片不会更新——哪怕你中途调用了 setMinutes,也只是「预约」了下一次渲染,当前照片不变。

console.log(minutes); // 0(第 0 次渲染的快照)
setMinutes(1);        // 通知 React:下一次渲染用 1
console.log(minutes); // 还是 0,快照不会中途改变

所以循环里的五次 setMinutes(minutes + 1) 实际都是 setMinutes(0 + 1)——五次都预约「下一次变成 1」,最终结果自然就是 1。React 还会把同一个事件处理函数里的多次 setState 合并成一次渲染(批处理),所以连界面刷新都只有一次。

修复方法是函数式更新:给 setState 传一个函数,参数是「最新的状态值」,返回「下一个状态值」。

function addFiveFixed() {
  for (let i = 0; i < 5; i++) {
    setMinutes(m => m + 1); // 每次 m 都是上一次计算的结果
  }
}

这五次更新会排进一个队列:0 => +1 => +1 => +1 => +1 => +1,最终正确得到 5。React 官方把这个队列机制叫「更新队列」(update queue),你不需要手工管理它,只需要记住:新状态依赖旧状态时,一律用函数式更新。

示例拆解

一个最小可运行的对照实验,两个按钮分别用两种写法:

import { useState } from "react";

export default function Counter() {
  const [count, setCount] = useState(0);

  return (
    <div>
      <p>当前:{count}</p>
      {/* 错误示范:连点三次也只加 1 */}
      <button onClick={() => { setCount(count + 1); setCount(count + 1); setCount(count + 1); }}>
        快照式 +1(点三次试试)
      </button>
      {/* 正确做法:函数式更新,点三次真的加 3 */}
      <button onClick={() => { setCount(c => c + 1); setCount(c => c + 1); setCount(c => c + 1); }}>
        函数式 +3
      </button>
    </div>
  );
}

逐行看关键点:

  • useState(0) 只在首次渲染真正「初始化」,之后调用它返回的都是当次渲染的快照。
  • 第一个按钮的三次 setCount(count + 1) 用的是同一个快照 count,三次都预约 count + 1,结果只加 1。
  • 第二个按钮的 c => c + 1 里,c 是队列里前一步算出的值,三次排队执行,结果加 3。

动手任务

打开本课实验区,完成两个小改动:

  1. 把「快照式 +1」按钮改成点击后真的加 2(不许改成函数式,试试还有没有别的办法——提示:直接写 setCount(count + 2))。
  2. 新增一个「重置」按钮,用函数式更新把计数归零。

改完在实验区点按钮验证:快照式按钮点三次 +2 应该只加 2,函数式 +3 每次都加 3。

常见错误

  • 在 setState 之后立刻读取状态:setCount(1); console.log(count) 打印的还是旧值。状态更新发生在下一次渲染,不是当场生效。
  • 在函数式更新里写副作用:setCount(c => { fetch(...); return c + 1 }) —— 更新函数必须是纯计算,副作用应该放在事件处理或 useEffect 里。
  • 把函数式更新当成性能优化:它不是为了省一次渲染,而是为了正确性。两种写法渲染次数一样。
  • 依赖旧快照做条件判断:if (count < 10) setCount(count + 1) 在快速连点时判断依据是旧快照,应该写成 setCount(c => (c < 10 ? c + 1 : c))。

检查与复盘

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

  • 能用自己的话说清「快照」是什么,为什么 setCount(count + 1) 连点会丢更新
  • 知道批处理:同一个事件里的多次 setState 合并成一次渲染
  • 写代码时能不假思索地选择:新值依赖旧值 → 函数式更新

参考资料

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

组件里 count 当前为 0。点击按钮后:先执行 setCount(5),再执行 setTimeout(() => alert(count), 0)。alert 弹出的值是?(React 18+)

Q2读代码较难

阅读代码(React 18+ 自动批处理)。页面初始显示「分数 0」。点击一次「胜利」按钮后,alert 弹出什么?按钮下方的分数最终显示什么?

function Score() {
  const [score, setScore] = useState(0);

  function onWin() {
    setScore(score + 10);
    setScore((s) => s + 5);
    alert(score);
  }

  return (
    <>
      <button onClick={onWin}>胜利</button>
      <p>分数 {score}</p>
    </>
  );
}
Q3故障定位中等

bug 现场:向导共 3 步,开发者的意图是「第 3 步点下一步时 step 变成 4,此时提交表单」。实际在第 3 步点击没有任何反应,要点第二次才提交,界面还显示成了「第 5 步」。原因和最可靠的修法是?

function Wizard() {
  const [step, setStep] = useState(1);

  function handleNext() {
    setStep(step + 1);
    if (step === 4) {
      // 以为 setStep 之后 step 就变成 4 了
      submit();
    }
  }

  return (
    <>
      <p>第 {step} 步 / 共 3 步</p>
      <button onClick={handleNext}>下一步</button>
    </>
  );
}

读完这一课了?

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

Command Palette

Search for a command to run...