React Context Provider

3 min read
Zekari
ReactContext API状态管理

什么是 Context

Context 解决了一个简单的问题:如何在组件树中传递数据,而不需要每一层都手动传递 props。

这不是状态管理。Context 本身不存储状态。它只是一个管道。数据从上游流向下游。

💡 Click the maximize icon to view in fullscreen

Provider 把数据放在树的顶端。任何层级的组件都可以直接读取,不需要中间层传递。

为什么容易误用

大部分问题源于一个混淆:把 Context 当作 Redux 或 Zustand。

Context 不管理状态。它只是传递数据。状态管理是另一回事。你可以用 useState 配合 Context,但这不意味着 Context 本身在管理状态。

这种混淆并不罕见。外表相似的东西往往掩盖本质的分野。Context Provider 和状态管理库都提供数据,但机制完全不同。

// 这不是状态管理
const ThemeContext = createContext('light')

// 这才是状态管理
const [theme, setTheme] = useState('light')

当你把 Context 当作状态管理工具,你会遇到性能问题。因为 Context 的设计目标不是优化频繁更新。

重渲染的代价

Context 的值改变时,所有使用它的组件都会重新渲染。

const UserContext = createContext()

function App() {
  const [user, setUser] = useState({ name: 'Alice', theme: 'dark' })

  return (
    <UserContext.Provider value={user}>
      <Navbar />      {/* 重渲染 */}
      <Sidebar />     {/* 重渲染 */}
      <MainContent /> {/* 重渲染 */}
    </UserContext.Provider>
  )
}

user 对象改变时,即使 Navbar 只关心 theme,它也会重新渲染。因为整个 user 对象变了。

这是设计上的权衡。Context 优先考虑简单性,而不是性能。

💡 Click the maximize icon to view in fullscreen

什么时候用 Context

Context 适合不常改变的数据。

主题设置。用户信息。语言偏好。这些数据改变频率低,但需要在很多地方读取。

// 好的使用场景
const ThemeContext = createContext()
const LocaleContext = createContext()
const AuthContext = createContext()

// 不好的使用场景
const FormDataContext = createContext()  // 表单数据变化频繁
const MousePositionContext = createContext()  // 鼠标位置每帧都变

如果数据每秒改变多次,不要用 Context。用专门的状态管理库

拆分 Context

一个大的 Context 会导致不必要的重渲染。拆分它。

// 不好:所有数据放在一起
const AppContext = createContext({
  user: {},
  theme: '',
  settings: {},
  notifications: []
})

// 好:按职责拆分
const UserContext = createContext()
const ThemeContext = createContext()
const SettingsContext = createContext()
const NotificationsContext = createContext()

组件只订阅它需要的 Context。Navbar 订阅 UserContextThemeContext,但不订阅 NotificationsContext

这样,通知数据改变时,Navbar 不会重渲染。

Provider 嵌套的问题

每次渲染都创建新对象会导致所有消费者重渲染。

// 错误:每次都创建新对象
function App() {
  const [theme, setTheme] = useState('dark')

  return (
    <ThemeContext.Provider value={{ theme, setTheme }}>
      {children}
    </ThemeContext.Provider>
  )
}

即使 theme 没变,value 对象每次都是新的。所有消费者都会重渲染。

// 正确:用 useMemo 稳定引用
function App() {
  const [theme, setTheme] = useState('dark')

  const value = useMemo(() => ({ theme, setTheme }), [theme])

  return (
    <ThemeContext.Provider value={value}>
      {children}
    </ThemeContext.Provider>
  )
}

useMemo 确保只有当 theme 真正改变时,value 对象才会重新创建。

这是最常见的 Context 性能问题。Provider 的 value prop 每次渲染都创建新对象。

React 用 Object.is 比较新旧值。如果对象引用不同,就触发重渲染。

解决方法:

  • useMemo 稳定对象引用
  • 或者把状态和 setter 分开放在不同的 Context

分离读写

把数据和更新函数放在不同的 Context 可以减少重渲染。

const ThemeContext = createContext()
const ThemeDispatchContext = createContext()

function ThemeProvider({ children }) {
  const [theme, setTheme] = useState('dark')

  return (
    <ThemeContext.Provider value={theme}>
      <ThemeDispatchContext.Provider value={setTheme}>
        {children}
      </ThemeDispatchContext.Provider>
    </ThemeContext.Provider>
  )
}

只读取主题的组件订阅 ThemeContext。只需要改变主题的组件订阅 ThemeDispatchContext

当主题改变时,只有读取主题的组件重渲染。更新函数的引用不变,订阅它的组件不会重渲染。

Context 不是银弹

Context 解决了 prop drilling 的问题,但引入了新问题:隐式依赖。

function UserProfile() {
  const user = useContext(UserContext)
  return <div>{user.name}</div>
}

从代码上看不出 UserProfile 依赖什么。你必须知道它用了 UserContext

对比显式的 props:

function UserProfile({ user }) {
  return <div>{user.name}</div>
}

依赖关系一目了然。

这是权衡。Context 让代码更简洁,但牺牲了明确性。选择取决于你的场景。

最后

Context 是工具,不是规则。不是所有数据都应该放在 Context 里。

有些数据适合 props。有些适合状态管理库。有些适合服务器状态库。

Context 适合那些不常改变、需要全局访问的数据。知道它的边界,才能用好它。

何时使用 Context:

  • 主题和样式配置
  • 用户认证状态
  • 语言和国际化设置
  • 特性开关(feature flags)

何时避免 Context:

  • 高频更新的数据(表单输入、动画状态)
  • 复杂的状态逻辑(购物车、多步骤表单)
  • 服务器状态(API 数据、缓存)
  • 需要时间旅行调试的状态

性能优化清单:

  • ✓ 用 useMemo 稳定 Provider 的 value
  • ✓ 拆分大的 Context 为多个小的
  • ✓ 考虑分离读写 Context
  • ✓ 用 memo() 包裹不需要频繁更新的消费者组件

参考资源