Context 的边界:跨层传值,不等于全局抽屉
问题:中间组件不使用主题,却要转交三层 props
props 能明确显示依赖,但纯转交层多时会制造噪声。Context 允许祖先为一棵子树提供值,深层组件读取离自己最近的 Provider。它解决“树中传递”,不自动决定 state 应放哪里,也不是服务器缓存或权限系统。
完整实验:嵌套主题作用域
import { createContext, useContext, useState } from 'react'
const ThemeContext = createContext(null)
function useTheme() {
const value = useContext(ThemeContext)
if (value === null) {
throw new Error('useTheme 必须在 ThemeContext 内使用')
}
return value
}
function Toolbar({ children }) {
return <section>{children}</section>
}
function ThemedButton({ children }) {
const theme = useTheme()
return (
<button style={{
color: theme === 'dark' ? 'white' : 'black',
background: theme === 'dark' ? '#333' : '#eee',
}}>
{children}({theme})
</button>
)
}
export default function App() {
const [theme, setTheme] = useState('light')
return (
<ThemeContext value={theme}>
<main>
<h1>主题作用域</h1>
<button onClick={() => setTheme(current => (
current === 'light' ? 'dark' : 'light'
))}>
切换外层主题
</button>
<Toolbar>
<ThemedButton>跟随外层</ThemedButton>
<ThemeContext value="dark">
<Toolbar>
<ThemedButton>固定深色子树</ThemedButton>
</Toolbar>
</ThemeContext>
</Toolbar>
</main>
</ThemeContext>
)
}这是 React 19 的 Provider 简写:<ThemeContext value={theme}>。旧代码常见 <ThemeContext.Provider value={theme}>,表达同一类提供关系。
从消费者向上查找最近值
外层按钮沿树向上找到动态 theme;内层按钮先遇到固定为 dark 的 Provider,因此外层切换不会改变它读到的主题值。这不表示内层组件不会随着父组件更新而重新执行:值的作用域与渲染次数要分开观察。Context 穿过 Toolbar,中间组件无需声明或转交主题 prop。
createContext(null) 的默认值只在上方没有任何对应 Provider 时使用,而且是静态后备值。对于必须提供的依赖,静默默认值会掩盖漏包 Provider;useTheme 立即抛错,让结构错误靠近根因。
Context 没有夺走 state 所有权
theme 仍由 App 的 useState 拥有。Context 只是运输通道。若把购物车、搜索草稿、服务器响应、权限和主题塞进同一个对象,任何 value 身份变化都会让读取该 Context 的消费者重新渲染,也让依赖来源变难追踪。
可按变化频率和职责拆分,例如 ThemeContext、CartStateContext、CartDispatchContext。拆分 state 与 dispatch 后,只派发事件的按钮不必读取不断变化的 state。
改一个条件:先试 children 组合
如果只有一个中间层,可以让真正使用主题的按钮接收 prop,把已经配置好的按钮作为 children 传给布局:
function PlainButton({ theme, children }) {
return <button className={theme}>{children}({theme})</button>
}
// 放在拥有 theme state 的父组件返回值中:
<Toolbar>
<PlainButton theme={theme}>显式接收主题</PlainButton>
</Toolbar>Toolbar 只安排 children 的位置,不必了解 theme;PlainButton 仍通过 prop 明确获得主题。仅把原来的 ThemedButton 放进 children 并不会消除 Context 依赖,因为它内部还在调用 useTheme。选择组合方案时,需要同时审视最终消费者怎样读取数据。
适合 Context 的值通常是树范围内许多远端节点都需要的依赖:主题、语言、当前账户视图、路由或一段 reducer state。只给一两个近邻使用的值继续传 props 更容易复用组件。
边界
- Provider 的范围是 JSX 子树,不是模块或整个浏览器;同一页面可有多个互不影响的 Provider。
useContext读取调用组件上方的 Provider,不能读取组件自己在返回 JSX 中新建的 Provider。- Context 值来自客户端不能证明用户权限;服务端仍须校验。
- Context 不是外部 Store 的细粒度 selector。性能问题先检查 Provider 范围和 value 职责,而不是盲目给所有值加缓存。
练习:可嵌套的单位设置
实现完整 App.jsx,创建 UnitContext 和 useUnit()。
契约:
- Context 默认值为
null,useUnit在缺少 Provider 时抛出明确错误; - 外层 Provider 可在
metric与imperial间切换,Distance把输入米数显示为米或英尺(1 m = 3.28084 ft,保留两位); - 一个内层 Provider 固定为
metric,证明最近 Provider 覆盖外层值; Layout只渲染 children,不接收或转交 unit prop;不得把换算结果存进 state。
完成后对照答案 02。