先记住这个答案
useReducer 将状态转移集中在 reducer 中,Context 则让后代读取 state 或拿到 dispatch。把两者放进不同 Context 后,只需发送动作的组件可以只消费 dispatch 通道。React 保证同一个挂载实例的 dispatch 身份稳定,因此 state 改变不必同时改变 dispatch Context 的值。不过拆分不会自动屏蔽父组件渲染,也不是所有小型状态都必须使用的固定结构。
- useReducer 描述转移规则,Context 负责树内分发
- 直接传稳定的 dispatch,避免重新包装成新对象
- 拆分减少无关订阅,不承诺组件永远不再渲染
把命令入口与展示输入分开
以库存编辑页为例,清单需要读取每个商品的库存,工具栏中的重置按钮可能只负责发出 reset 动作。若两者都读取包含 state 和 dispatch 的同一 Context,每次库存变化都会改变这个组合对象,工具栏也消费了发生变化的值。分开后,重置按钮可以完全不读取库存状态。
状态仍由同一个 useReducer 管理,并没有因为两个 Context 就变成两份独立来源。reducer 应保持纯粹,用动作表达业务变化;请求、日志上报等副作用需要另行安排。动作携带什么参数也应清楚,避免把任意字段写入都包装成缺乏业务含义的万能更新命令。
稳定 dispatch 的收益要落实到 Provider value
将 dispatch 函数直接作为动作 Context 的 value,可以利用它在该组件实例内的稳定身份。如果每次渲染又写成包含 dispatch 的新对象,虽然对象里的函数没变,整个 value 仍然是新引用。确实需要提供多个动作时,再考虑构建稳定的动作对象与完整依赖。
拆分之后也要看组件树。Provider 所在父组件更新,仍可能沿普通子元素渲染路径带来工作;只读 dispatch 的按钮不是因此就一定零次渲染。需要进一步优化时,可以结合组件组合、memo 与性能测量,把 Context 通知的改善和父层渲染的改善分别记录。
什么情况下值得采用这种组织方式
多个操作共同影响一份有约束的状态时,reducer 有助于统一处理转移,例如增加库存不能越过上限,撤销需要恢复一组字段。深层组件又需要提交这些操作时,Context 能减少单纯传递参数的中间层。两者组合的价值来自业务组织,而不是 Hook 数量越多越专业。
如果只有两个近邻组件共享一个布尔值,提升 state 后传 props 往往已经足够。即便使用 reducer,小规模页面也可以先用单一 Context,待读取与操作需求分化再拆。判断依据是可理解性、消费者分布和已测得的成本,不能给所有页面规定同一套 Provider 层数。
容易答错的地方
- dispatch 稳定,所以包含它的对象也稳定
- 对象字面量每次执行都会创建新的引用,属性中的函数稳定并不会让外层对象自动复用。检查 value 时应看最终传入的值,而不只是其中一个字段。
- 每次 dispatch 一定让 state Context 改变
- reducer 可以返回此前的状态对象,React 会依据 Object.is 判断相同状态并跳过相应更新工作。不要为了制造变化而无条件复制状态,也不要原地修改后返回旧对象。
面试官还会怎么问?
dispatch 可以从 Effect 依赖中省略吗?
它具有稳定身份,React 的依赖检查允许省略时可以省略,加入它也不会因此反复触发 Effect。Effect 中读取的其它响应式输入仍需完整处理,不能借此忽略真正变化的值。
多个 Context 会让状态更新失去一致性吗?
Context 只是分发方式。若数据仍由同一个 reducer 和所有者产生,可以从同一份状态生成多个值;真正需要避免的是把有关联的权威状态复制到不同地方再靠 Effect 互相同步。
只消费 dispatch 的组件怎样显示禁用状态?
如果禁用状态取决于当前库存或权限,它就确实需要相应输入。可以额外读取所需状态或从外层传入布尔值,不能为了少一次订阅让按钮显示过期的可操作状态。
参考资料
示例用于理解所注明的运行环境与边界;延伸学习可结合原文中的更多案例。