先决定谁拥有状态
一个值应该放在哪里,取决于它的来源、使用范围和生命周期。输入框的草稿通常属于表单组件;多个兄弟组件都需要的筛选条件可以由共同父级拥有;可从其他状态计算出来的值不应再复制一份。
ts
type CartLine = { price: number; quantity: number };
const lines: CartLine[] = [
{ price: 10, quantity: 2 },
{ price: 5, quantity: 1 }
];
const total = lines.reduce((sum, line) => sum + line.price * line.quantity, 0);total 是派生值。把它同时存进 state 会产生两个真相来源:更新商品数量时,代码必须记得同步修改 total,而遗漏就会显示旧结果。
三个判断问题
- 这个值能否由已有状态和输入稳定计算?能就优先派生。
- 谁需要读取和修改它?把所有权放在最近的共同边界。
- 它何时失效?组件卸载、路由离开、用户登出和服务端更新都可能结束它的生命周期。
练习
设计一个商品筛选页的状态模型:搜索词、分类、排序、结果列表、加载状态和错误。指出哪些状态由 URL 表示,哪些是服务端状态,哪些只是本地派生值。
核心机制与实现
状态设计先区分本地 UI、跨组件共享、服务器缓存和 URL 状态;全局 store 不是默认答案。
围绕“先决定谁拥有状态”先写出输入 → 中间状态 → 可观察结果,再实现最小闭环。不要把事件监听、异步请求、DOM 读取或 store 写入混在一个函数里;每个副作用都要有创建、完成、取消和清理路径。
js
type State = { filter: string; selectedId: string | null }
const initial: State = { filter: '', selectedId: null }
function reducer(state: State, action: Action): State {
if (action.type === 'filter/changed') return { ...state, filter: action.value }
return state
}逐步运行:先只验证成功路径,再改变一个输入;记录时间顺序、对象身份、错误类型和最终可见状态。若环境不支持某个浏览器 API,要把静态协议验证和真实浏览器验证分开记录。
失败边界与变体
- 输入为空、重复、过期或非法时,不应静默覆盖有效状态。
- 组件卸载、路由离开、标签页关闭或请求取消时,资源必须停止写入。
- 跨文档、Shadow DOM、Worker、SSR 或多标签页会改变可见边界,不能沿用单页面直觉。
- 需要持久化时记录 schema 版本和迁移失败策略;需要全局状态时先证明局部状态不足。
可运行实验与练习
画出状态所有者、读者和写入者;测试刷新、回退、并发请求、错误恢复和持久化迁移,解释为何某个状态不应放进全局 store。
练习要求:实现一个最小版本,补一个失败测试,再给出一个替代方案及其适用条件。答案应包括实际输入序列、观察结果、修复原因和未覆盖的浏览器差异。