先记住这个答案
useContext 查找组件上方最近的匹配 Provider,匹配依据是同一个 Context 对象,而不是变量名称相同。嵌套 Provider 会为自己的后代提供完整的新 value,不会自动把对象字段与外层合并。没有匹配 Provider 时才使用 createContext 的默认值;如果最近 Provider 明确提供 undefined,消费者得到的就是 undefined。
- 匹配依据是 Context 对象身份,变量同名不够
- 内层 value 整体替换,字段合并需要自己表达
- 读取发生在当前组件上方,不会读取自己返回的 Provider
局部主题为什么不会污染页面其它区域
假设应用整体使用浅色主题,而代码预览区域需要深色背景。可以在预览区域外围再放一个同一 ThemeContext 的 Provider。预览中的消费者读取局部深色值,旁边的导航仍读取外层浅色值;不是先修改全局主题再在离开预览时恢复。
嵌套的前提是同一个 Context 对象。两个文件各自调用 createContext,即使都把变量命名为 ThemeContext,也建立了不同通道。模块被重复打包时同样要留意对象身份是否一致,否则提供者看起来存在,消费者却一直得到另一份 Context 的默认值。
对象覆盖与对象合并是不同操作
外层配置若包含 color 和 density,内层仅提供包含 color 的对象,内层消费者不会自动继承 density。想表达局部扩展,可以在包装组件中先用 useContext 读取外层配置,再构造包含旧字段与局部覆盖的新对象,把这个新对象交给返回的 Provider。
这里包装组件读到外层值,是因为 useContext 只考虑当前组件上方的提供者,不会把当前组件返回的 Provider 当作自己的祖先。合并时还要定义嵌套对象的规则:对象展开只是浅合并,若 tokens 内部也要保留外层字段,应该明确合并哪些字段,而不是让配置行为依赖偶然的对象结构。
默认值、生命周期与服务端的边界
默认值用于树上没有匹配 Provider 的情形。最近 Provider 的 value 是 undefined,也已经构成明确的提供关系,不会继续向外寻找一个看起来更有用的值。对于必须在提供者内使用的业务 Hook,可以用空标记默认值并抛出清晰错误,尽早发现挂载位置不对。
普通 React 服务端渲染也按照树结构确定 Provider 范围,客户端水合则需要一致的初始输入。不能由此推断 React Server Components 可以读取客户端 Context:两种服务端概念要分开。路由切换若卸载了某个 Provider,其内部状态也会按组件生命周期结束,嵌套规则不会自动提供持久化。
容易答错的地方
- 外层变更必然更新所有内层消费者
- 内层消费者读取的是最近匹配的值。外层变化可能通过父层渲染或显式合并影响内层,但不能直接把外层 Context 通知当作内层输入已经变化的证据。
- 不同 Context 嵌套会相互覆盖
- 不同 Context 对象提供不同的数据通道,消费者可以分别读取它们。只有同一 Context 的更近 Provider 才改变该通道的读取来源,不能用变量名字判断覆盖关系。
面试官还会怎么问?
Provider 没传 value 会退回默认值吗?
不会把缺失的 value 当成 Provider 不存在;其值相当于 undefined。应检查属性拼写与数据初始化,避免用默认值误掩盖提供者配置错误。
在创建 Provider 的组件中调用 useContext 会读到新值吗?
不会。这个调用读取的是当前组件上方的提供者。要读取它返回的 Provider,应在该 Provider 的后代组件中调用 useContext。
React Portal 会按照 DOM 容器寻找 Provider 吗?
Context 归属跟随 React 树,不能根据 portal 挂载到的 DOM 容器推断其提供者。设计跨区域弹层时,应确认它在 React 组件树中的位置与实际需要的业务范围一致。
参考资料
示例用于理解所注明的运行环境与边界;延伸学习可结合原文中的更多案例。