先记住这个答案
Context 可以传递任意适合作为 value 的值,包括 setter、dispatch 和业务函数。父组件持有 state,把更新入口交给 Context;深层按钮通过 useContext 取得入口,在点击等事件中请求更新。React 随后依据新的父层状态渲染,后代再读取相应值。这仍然是有明确状态来源的数据流,不能把它理解成子组件可以任意修改 Context 对象并自动刷新界面。
- 状态留在明确的所有者,子组件只提交更新请求
- 根据旧值计算下一值时,优先考虑函数式更新
- 函数身份稳定是优化条件,不是更新能够生效的前提
setter、dispatch 与业务命令怎样选择
简单开关可以直接提供 setter,消费者明确设置目标值。涉及多个字段和业务规则时,可以提供 dispatch,让 reducer 统一处理动作;也可以公开名为 archiveDocument 或 toggleTheme 的函数,让调用方不用知道状态结构。三种方式都需要明确谁负责校验输入和处理失败。
如果一个子组件只能发起归档,却拿到了可覆盖整份文档状态的 setter,它的权限范围可能比业务需要更大。这里的范围首先是代码组织问题:收窄命令入口能减少误改字段的机会,但前端函数封装不能代替服务端权限检查,真实写入仍需在可信边界验证。
切换主题时避免读取旧闭包
例如父层定义 toggleTheme,在其中调用 setTheme(previous => previous === "light" ? "dark" : "light"),就把依赖旧主题计算新主题的逻辑交给更新函数。深层按钮点击时调用这个入口即可,不需要先读取主题再猜测下一次提交时仍然是哪一个值。
如果需要让入口在无关渲染之间保持引用,可以使用 useCallback,并列全它读取的响应式依赖。上述函数式写法只使用稳定 setter 与常量时可以使用空依赖;若函数还读取某个 prop,就必须处理该 prop 的变化。不要为了固定引用而让命令长期使用旧的业务参数。
动作对象和调用时机也要检查
多个函数被装进一个动作对象后,最终 Provider value 的身份取决于这个对象,不能只看到每个函数稳定就认为值也稳定。必要时对动作对象进行缓存,或直接提供稳定的 dispatch。是否拆开状态与动作通道,应结合哪些组件只发命令、哪些组件真正需要读状态。
调用更新入口通常发生在用户事件或恰当的外部同步流程中,不应在组件渲染时无条件调用父层 setter,否则容易出现渲染期间更新其它组件的警告或循环。异步命令还要明确加载、失败与迟到结果的处理,Context 只负责把入口送到调用者,并不会自动提供这些控制。
容易答错的地方
- 直接修改 useContext 返回对象的字段
- 普通字段赋值没有向 React 提交状态更新,消费者可能继续展示旧快照。应调用所有者提供的更新入口;如果传的是外部可变 store,则必须按其订阅协议使用。
- 没有 useCallback 就不能通过 Context 更新
- 普通函数也可以正确调用 setter。useCallback 影响的是引用复用与相关优化,正确性来自函数逻辑、依赖和调用时机,不能把缓存 Hook 当成状态更新开关。
面试官还会怎么问?
原生 setter 需要再包 useCallback 吗?
setter 身份本身稳定,直接传递时通常没有必要再包一层。需要加入业务转换或校验时可以定义包装函数,再根据性能与依赖需求决定是否缓存。
为什么子组件点击后其它区域也更新了?
它们可能消费了同一份改变的 Context,也可能随着父组件渲染执行。先确认真实数据依赖,再考虑拆分 Context 或减少父层传播,不能因为触发点很深就假设更新只影响那个按钮。
多个深层组件都能修改同一状态会冲突吗?
可能出现业务层面的竞争,需要用函数式更新、统一 reducer 或请求版本等方式表达顺序。Context 不会自动合并互相覆盖的异步结果,所有者必须明确冲突与失败策略。
参考资料
示例用于理解所注明的运行环境与边界;延伸学习可结合原文中的更多案例。