Skip to content

Context 的边界:跨层传值,不等于全局抽屉 ​

问题:中间组件不使用主题,却要转交三层 props ​

props 能明确显示依赖,但纯转交层多时会制造噪声。Context 允许祖先为一棵子树提供值,深层组件读取离自己最近的 Provider。它解决“树中传递”,不自动决定 state 应放哪里,也不是服务器缓存或权限系统。

完整实验:嵌套主题作用域 ​

jsx
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 传给布局:

jsx
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。

下一篇 ​

自定义 Hook 与外部 Store

用心记录每一步成长