先记住这个答案
useContext 从当前组件向上寻找同一个 Context 对象对应的最近 Provider;找不到时,才返回 createContext 的默认值。这个默认值不会随组件状态自动更新。若找到了 Provider,其 value 即使是 undefined 或 null,也就是提供给消费者的值,React 不会把它视作“缺少 Provider”。嵌套 Provider 则由最近的一层覆盖外层。
- 没有匹配 Provider 才读取默认值
- Provider 的 undefined 是实际值,不触发兜底
- 匹配条件既包含树位置,也包含 Context 对象身份
默认值与 Provider 初始状态有何不同
假设主题 Context 的默认值为 light,某个按钮没有被主题 Provider 包裹,它就读取 light。如果上层 Provider 的初始 state 为 dark,按钮会读取 dark;默认值不是会与 Provider 状态合并的配置对象,也不是每次提供值为空时重新执行的回退逻辑。
这一点在异步加载时尤其容易混淆。若 Provider 正在等待配置并传入 undefined,消费者确实会得到 undefined。应在 Provider 层表达加载状态或提供完整初值,而不是期待 createContext 的默认对象自动补齐缺少字段。消费者也应根据约定处理可空值,避免直接解构导致报错。
嵌套覆盖与同组件读取的区别
一个页面可能在整站主题 Provider 内,再为预览区域嵌套另一层主题 Provider。预览区内部的按钮读取内层值,其外面的按钮读取外层值。覆盖只影响对应子树,不会修改外层 Provider 的状态,也不会改变其它分支消费者读取到的结果。
如果某个组件先调用 useContext,然后在返回的 JSX 中创建 Provider,这次调用不会读取自己返回的那个 Provider,因为它不在当前组件上方。需要让读取逻辑进入这个 Provider 的后代组件,或者把 Provider 提到更高一层。检查 JSX 外观时尤其要分清组件内部返回树与调用所在的组件节点。
共享状态设计与模块身份排查
对于离开 Provider 就没有业务意义的会话上下文,可以使用可空默认值,并通过专用 Hook 检查是否存在提供值,让调用方得到清晰错误。若组件脱离 Provider 也应该正常展示,例如默认主题,则可以提供有意义的默认值。两者是产品契约选择,不是某种默认写法总是更高级。
若明明看到了 Provider 却一直读取默认值,要检查导入是否来自同一个模块实例。重复打包、错误导出或在组件执行期间重新 createContext,都可能让提供者和消费者使用不同对象。不要先加层层 fallback 掩盖这种身份错配,应先确认树位置、Context 引用和 value 传递。
容易答错的地方
- 认为 Context 默认值会浅合并 Provider 对象
- React 不会自动合并默认对象与 value。Provider 只给出部分字段时,消费者就得到那份部分对象;如果需要默认配置合并,应在应用自己的数据层明确实现并验证。
- 认为同名 Context 一定能够匹配
- 变量名相同并不代表对象身份相同。不同模块各自 createContext,或依赖重复加载导致模块存在多份实例,都可能形成互不对应的提供者与消费者。
面试官还会怎么问?
Provider 传 null 与没有 Provider 一样吗?
不一样。传 null 表示最近提供者的值就是 null;没有匹配的提供者才回退到 createContext 的默认值。业务如果用 null 表达未登录,应与缺失配置等情况建立清楚的约定。
改变创建 Context 时传入对象的字段能更新消费者吗?
直接修改普通对象字段不等于触发 React 的更新传播,默认值也不应被当成可变全局状态使用。需要变化的数据应由 Provider 的状态或其它支持订阅的数据源提供。
把读取 Context 的组件用 memo 包裹会影响寻找默认值吗?
不会改变 Context 的查找规则。memo 是 props 层面的优化工具;组件仍然按照树关系和 Context 身份读取值,消费的 Context 发生变化也可能触发它重新渲染。
参考资料
示例用于理解所注明的运行环境与边界;延伸学习可结合原文中的更多案例。