先记住这个答案
派生值如果完全由当前 props 或 state 决定,通常直接在渲染中计算,不再用一份 state 保存它。用 Effect 更新派生 state,会让一次输入变化先得到包含旧派生值的渲染,再安排一次更新;同时增加两份数据必须保持一致的负担。计算确实昂贵时可评估 useMemo,用户能独立编辑、撤销或保留的草稿则是另一类状态,不能简单删掉。
- 保存最少的独立状态,展示值从它推导
- Effect 同步外部系统,不负责重复计算展示值
- useMemo 用于成本优化,不改变数据的来源
为什么多存一份结果会制造时序问题
假设订单里保存了商品数量,又通过 Effect 把总价写到另一个 state。数量从一改成二时,组件先拿到新数量与旧总价,Effect 执行后才产生新总价。若其他逻辑读取这次提交,就必须解释两者暂时不一致的情况,而直接用数量和单价计算不会建立这种同步缺口。
这不是要求把所有工作塞进 render。计算必须纯粹,不能顺便写数据库、修改传入数组或启动请求。纯计算可能随着渲染重试再次执行,结果应只依赖显式输入;外部操作则需要事件处理器、数据层或恰当的 Effect 生命周期。
订单合计与可编辑报价不是同一种状态
订单小计、筛选后的商品和是否达到免邮门槛,可以从商品列表、数量和地区规则推导。把这些结果同时保存,容易出现只更新其中一项的维护错误。测试时应覆盖商品删除、数量变化和地区切换,并检查同一组输入始终得到一致的展示结果。
但销售人员手动修改的报价不是商品价格的简单映射:它可能需要保留折扣理由、审批状态以及撤销记录。这时应把自动建议价和已编辑报价区分开,明确什么时候重新计算建议、什么时候保留人工输入,不能借“避免派生状态”覆盖用户尚未保存的编辑。
什么时候需要缓存,以及怎样判断收益
如果合计只是几次乘加,直接计算通常比增加缓存管理更清楚。面对较大的搜索索引或复杂聚合,可以先测量真实交互中的成本,再用 useMemo 缓存纯计算。依赖必须完整,输入的引用变化也应符合状态不可变更新的约定。
不要把 useMemo 当成永不失效的存储,更不能为了稳定某个副作用执行次数而把业务正确性绑在缓存上。即使去掉缓存,页面也应给出正确结果。若数据量已经让主线程明显阻塞,还应考虑减少输入规模、调整算法或移动计算,而不是只增加一个 Hook。
容易答错的地方
- 把避免 Effect 理解成禁止 Effect
- 连接 WebSocket、清理订阅、同步第三方地图等确实涉及外部系统。问题在于用副作用把内部可计算的数据再复制一遍;判断依据是数据关系与同步目标,而不是看到 useEffect 就机械删除。
- 认为所有 props 初始化 state 都错误
- 有些控件需要从初始值建立独立草稿,后续输入有自己的生命周期。这种设计可以成立,但必须约定外部初始值改变时是否重置,以及如何处理未保存修改,不能默认为永远跟随 props。
面试官还会怎么问?
派生值每次渲染都计算,是否一定很慢?
不一定,要看计算规模和触发频率。简单布尔判断、少量合计通常没有可感知成本;应在真实交互中测量瓶颈,避免为了省几次运算引入更难维护的重复状态。
派生结果需要发送给第三方图表时怎么做?
先在渲染中得到纯粹的结果,再通过负责图表实例的同步逻辑更新外部对象。结果的计算与外部同步是两个职责,不能因为后者需要 Effect,就把前者也写成 setState 链。
多个组件都需要同一个派生值时放在哪里?
可以在共同父组件计算后传递,也可以放在共享状态层的选择器中。关键是让计算基于同一份权威输入,避免每个组件维护一份可以独立过期的结果副本。
参考资料
示例用于理解所注明的运行环境与边界;延伸学习可结合原文中的更多案例。