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

选择状态的最小集合

选择状态的最小集合 · 阅读后完成下方实验与检查题,再用「标记完成」记录进度。

学习目标

状态写得越多,同步成本越高。这一课教一个设计动作:对着已有的状态清单逐个追问「它能不能算出来」,把能算的删掉,换成渲染时的派生计算。读完你能把一个重复字段缠身的功能收敛到最小状态集。

问题场景

任务清单第一版存了三个 state:

const [tasks, setTasks] = useState([]);
const [doneCount, setDoneCount] = useState(0);      // 完成数量
const [visibleTasks, setVisibleTasks] = useState([]); // 筛选后的列表

勾选一个任务要连调三个 setState:改 tasks、算 doneCount、更新 visibleTasks。测试很快抓到 bug:快速连点两次勾选,完成数偶尔少 1——状态多了,忘同步只是时间问题。

核心解释

React 官方的判断标准只有一句话:如果一个值能由已有 props 或已有 state 算出来,它就不该是 state。 上面代码里,doneCount 是 tasks.filter(t => t.done).length,visibleTasks 是 tasks 过滤排序的结果——两者都是 tasks 的影子,删掉,渲染时现算:

const doneCount = tasks.filter((t) => t.done).length;
const visibleTasks =
  filter === "all" ? tasks : tasks.filter((t) => t.done);

每次渲染重新算一遍,永远和 tasks 一致,不存在「忘了同步」。任务量几百条时这点计算完全无感——先按正确性写,真出现性能问题再上 useMemo 缓存。

判断一个值该不该是 state,可以按顺序问三个问题:

  1. 它能否由 props 算出?能,就是派生值;
  2. 它能否由已有 state 算出?能,也是派生值;
  3. 它会随用户操作变化且算不出来吗?只有这才是 state。

还有一种典型冗余:把「初始值 + 变更历史」都存起来。比如编辑表单存 { draft, original } 两个字段,其实 original 挂载时存一次即可,draft 变化时 hasChanges 直接算 draft !== original。存最小事实,其余靠计算还原。

示例拆解

实验区是同一个任务清单的两种实现,关键点如下:

  • Bug 版:三个 state 各自为政,勾选时 setDoneCount 被故意写漏一个分支——勾选第 3 个任务后计数停在 2,界面自相矛盾;「手动同步」按钮展示了每次操作要写多少同步代码;
  • 修复版:只存 tasks 和 filter 两个 state,doneCount 与 visibleTasks 在渲染函数体里直接算,勾选处理只有一个 setTasks;
  • 两个版本的界面结构完全相同——说明删状态不影响功能,只影响正确性风险和维护成本;
  • 数据面板实时显示三者的值,修复版的派生值永远跟着 tasks 走。

验证方式:两边各快速勾选、取消勾选十次,Bug 版迟早露出不一致,修复版永远自洽。

动手任务

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

  1. 给修复版加「按名称排序」开关,观察你只需改派生计算那一行,状态零新增。
  2. 在 Bug 版补上漏掉的同步分支,数一数加这一个功能要改几处。
  3. 把修复版的 filter 也删掉试试——哪里坏了?想清楚 filter 为什么必须留(用户意图,算不出来)。

常见错误

  • 把派生值存成 state:多一个就多一处同步,迟早不一致;改成渲染时计算。
  • 在 useEffect 里手动同步派生值:useEffect(() => setDoneCount(...), [tasks]) 是把冗余状态又绕回来了,且多一次渲染;直接算。
  • 连 props 都复制进 state:const [copy] = useState(props.value) 之后 props 更新了 copy 不动,界面用旧值;props 就该直接用。
  • 反向优化过头:把「用户输入的搜索词」也试图从别处推导——它是用户意图的源头,算不出来,老老实实做 state。

检查与复盘

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

  • 写完功能后能对着 state 清单逐个追问「能不能算出来」
  • 派生计算直接写在渲染函数体里,不用 useEffect 同步
  • 能说出哪些值必须做 state:用户意图、外部输入、算不出来的事实

参考资料

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

设计「购物车」界面:展示来自接口的商品列表、可修改数量、显示总价、可全选。下列 state 设计符合「最小状态集」原则的是?

Q2故障定位较难

bug 现场:表单有姓、名两个输入框,另存了 fullName 想同步显示。输入名之后 fullName 总是少一个字,代码如下。问题出在哪?怎么修?

const [firstName, setFirstName] = useState('');
const [lastName, setLastName] = useState('');
const [fullName, setFullName] = useState('');

function handleFirst(v) {
  setFirstName(v);
  setFullName(v + lastName);
}
function handleLast(v) {
  setLastName(v);
  setFullName(firstName + v);
}

读完这一课了?

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

Command Palette

Search for a command to run...