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

派生状态与重复来源

派生状态与重复来源 · 阅读后完成下方实验与检查题,再用「标记完成」记录进度。

学习目标

这一课处理一个高频状态设计问题:把「能算出来的值」又存成了 state。读完你应该能识别这类冗余实现,把筛选、统计改回渲染期计算,并验证一次更新后所有展示保持一致。

问题场景

课程列表页有搜索框、筛选结果和统计条(共几门、已完成几门)。第一版把筛选结果和统计都存进 state,再用 useEffect 跟着搜索词同步:

const [query, setQuery] = useState("");
const [filteredList, setFilteredList] = useState(courses);
const [doneCount, setDoneCount] = useState(0);

useEffect(() => {
  const list = courses.filter(c => c.title.includes(query));
  setFilteredList(list);
  setDoneCount(list.filter(c => c.done).length);
}, [query]);

看起来能跑,但每加一个输入来源(比如「只看未完成」开关),你就得回 effect 里多写一行 setDoneCount。漏写一处,页面就会出现「列表变了、统计没变」的事故。

核心解释

问题的根源是同一份数据存了三份。courses 是唯一来源(source of truth),filteredList 和 doneCount 都是从它算出来的派生值。派生值一旦存成 state,就有了独立生命周期,必须靠 effect 手工保持同步——手工同步一定会漏。

React 的渲染模型给了更好的位置:渲染本身就是一次全量重算。query 变了,组件函数重新执行,直接在渲染期算出列表和统计:

const filteredList = courses.filter(c => c.title.includes(query));
const doneCount = filteredList.filter(c => c.done).length;

没有 effect、没有多余的 setState,三个展示项共享同一组输入,一次更新后必然一致——因为它们本来就是同一次计算算出来的。判断标准很简单:这个值能从已有的 state 和 props 推导出来吗?能,就不要存。

那 useMemo 什么时候用?只在这份计算真的很重(几千条数据的复杂变换)且依赖多数时候不变时,才值得用它缓存结果。它是性能优化,不是正确性来源——先把逻辑写成渲染期计算,再谈缓存。

示例拆解

实验区是修正后的课程筛选器,关键点如下:

const [query, setQuery] = useState("");
const [onlyTodo, setOnlyTodo] = useState(false);

// 派生:每次渲染现算,无 effect
const filtered = COURSES.filter(
  c => c.title.includes(query.trim()) && (!onlyTodo || !c.done)
);
const doneCount = filtered.filter(c => c.done).length;
  • 两个输入来源:搜索词 query 和开关 onlyTodo,各自一条 setState;
  • filtered 同时吃两个输入,不需要 effect 去监听「谁变了」;
  • doneCount 从 filtered 再派生,天然和列表一致;
  • 验证方式:勾上「只看未完成」再打字搜索,列表和「已完成 N 门」永远同步变化,不会出现先后脚。

动手任务

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

  1. 新增一个「已完成 N / 共 M 门」的统计条,直接从 filtered 派生,不许新增 state。
  2. 把 onlyTodo 改成三态(全部 / 未完成 / 已完成),确认统计仍然一次更新就正确。
  3. 找一个你写过的「state + effect 同步」片段,判断哪个字段其实是派生值,说出去掉后哪些 setState 可以删。

常见错误

  • props 变了忘了重置内部 state:把 propsX 转换后存进 state 再用 effect 同步;正确做法是渲染期直接算,或用 key 重置(下一类问题见 R21)。
  • 在 effect 里 setState 又依赖同一批值:形成「渲染 → effect → 再渲染」的额外轮次,白白多渲染一次。
  • 用 useMemo 当正确性工具:漏了依赖时 useMemo 会给出旧值;派生值先保证「每次渲染都算」,性能不够再缓存。
  • 把派生值存进对象 state 一起更新:比如 setResult({ list, done }),看似省事,实际又制造了一个要手工同步的组合状态。

检查与复盘

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

  • 能一眼判断一个 state 是不是派生值:能否仅凭现有 state 和 props 算出来
  • 知道 effect 同步方案的代价:多一轮渲染、漏同步风险、逻辑分散
  • 会用「改一个输入,看所有展示是否同步」来验证派生实现是否干净

参考资料

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

bug 现场:商品列表支持关键字筛选。代码把「筛选后的列表」存成 state,用 useEffect 监听 items 和 keyword 同步。出现两个毛病:首次渲染闪一下全量列表;快速输入时偶尔显示上一次的筛选结果。更好的做法是?

const [items, setItems] = useState(allItems);
const [keyword, setKeyword] = useState('');
const [visible, setVisible] = useState(items);

useEffect(() => {
  setVisible(items.filter((i) => i.name.includes(keyword)));
}, [items, keyword]);

读完这一课了?

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

Command Palette

Search for a command to run...