派生状态与重复来源
派生状态与重复来源 · 阅读后完成下方实验与检查题,再用「标记完成」记录进度。
学习目标
这一课处理一个高频状态设计问题:把「能算出来的值」又存成了 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 门」永远同步变化,不会出现先后脚。
动手任务
打开本课实验区,完成三个小改动:
- 新增一个「已完成 N / 共 M 门」的统计条,直接从
filtered派生,不许新增 state。 - 把
onlyTodo改成三态(全部 / 未完成 / 已完成),确认统计仍然一次更新就正确。 - 找一个你写过的「state + effect 同步」片段,判断哪个字段其实是派生值,说出去掉后哪些 setState 可以删。
常见错误
- props 变了忘了重置内部 state:把
propsX转换后存进 state 再用 effect 同步;正确做法是渲染期直接算,或用 key 重置(下一类问题见 R21)。 - 在 effect 里 setState 又依赖同一批值:形成「渲染 → effect → 再渲染」的额外轮次,白白多渲染一次。
- 用 useMemo 当正确性工具:漏了依赖时 useMemo 会给出旧值;派生值先保证「每次渲染都算」,性能不够再缓存。
- 把派生值存进对象 state 一起更新:比如
setResult({ list, done }),看似省事,实际又制造了一个要手工同步的组合状态。
检查与复盘
完成下方检查题确认理解。自查清单:
- 能一眼判断一个 state 是不是派生值:能否仅凭现有 state 和 props 算出来
- 知道 effect 同步方案的代价:多一轮渲染、漏同步风险、逻辑分散
- 会用「改一个输入,看所有展示是否同步」来验证派生实现是否干净
参考资料
- React 官方文档:你可能不需要 Effect —— 「把 effect 移回渲染期/事件处理」的完整决策表
- React 官方文档:选择 state 结构 —— 避免重复与矛盾状态的结构原则
读完这一课了?
标记完成后,工作台会安排接下来的学习。