FORMA

Context

Context 在组件树中跨层级传递数据,避免 props 逐层透传。见 组件状态管理

核心概念

没有 Context 时,跨越多层组件传递数据只能靠逐层透传 props("prop drilling")——中间的每一层组件即使自己不需要这份数据,也得帮忙往下传。Context 提供了一种「广播」机制:Provider 在树的某一层设置一个值,其下任意深度的组件都能用 useContext 直接读取,跳过中间层。

创建与使用

tsx
import { createContext, useContext, useState, type ReactNode } from "react";

type Theme = "light" | "dark";
// 默认值传 undefined(而非真实的 "light"),才能在 useTheme 中检测「未包裹 Provider」
const ThemeContext = createContext<Theme | undefined>(undefined);

export function ThemeProvider({ children }: { children: ReactNode }) {
  const [theme, setTheme] = useState<Theme>("light");
  return (
    <ThemeContext.Provider value={theme}>{children}</ThemeContext.Provider>
  );
}

export function useTheme() {
  const theme = useContext(ThemeContext);
  if (theme === undefined) {
    throw new Error("useTheme 须在 ThemeProvider 内使用");
  }
  return theme;
}
实践说明
导出 自定义 Hook封装 useContext,统一校验与类型
Provider 靠近根节点仅包裹需要该数据的子树
拆分 Context频繁变化的数据与稳定配置分 Context,减少无关重渲染

完整示例:带 setter 的 Context

多数实际场景中 Context 不仅传值,还要传更新方法:

tsx
interface ThemeContextValue {
  theme: Theme;
  toggleTheme: () => void;
}

const ThemeContext = createContext<ThemeContextValue | undefined>(undefined);

export function ThemeProvider({ children }: { children: ReactNode }) {
  const [theme, setTheme] = useState<Theme>("light");
  const toggleTheme = useCallback(() => {
    setTheme((t) => (t === "light" ? "dark" : "light"));
  }, []);

  // 用 useMemo 稳定 value 引用,避免每次 Provider 渲染都创建新对象
  const value = useMemo(() => ({ theme, toggleTheme }), [theme, toggleTheme]);

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

export function useTheme() {
  const ctx = useContext(ThemeContext);
  if (!ctx) throw new Error("useTheme 须在 ThemeProvider 内使用");
  return ctx;
}

关键点value 对象若每次渲染都重新创建(未 useMemo),所有消费该 Context 的组件都会认为「值变了」而重渲染,即使 theme 实际没变。

拆分 Context 提升性能

tsx
// 不佳:所有数据塞进一个 Context,任何字段变化都导致全部消费者重渲染
const AppContext = createContext<
  { user: User; theme: Theme; notifications: Notification[] } | undefined
>(undefined); // createContext 必须传默认值参数

// 更好:按更新频率拆分
const UserContext = createContext<User | undefined>(undefined);   // 低频
const ThemeContext = createContext<Theme>("light");                 // 低频
const NotificationsContext = createContext<Notification[]>([]);     // 高频

只订阅 NotificationsContext 的组件不会因为 theme 切换而重渲染,反之亦然。

Context 与全局状态库

ContextRedux / Zustand 等
内置需安装
适用主题、语言、鉴权用户信息等低频更新复杂全局状态、时间旅行、中间件
性能消费方在 value 变化时重渲染库通常有更细粒度订阅
更新粒度无法只订阅某个字段(除非手动拆多个 Context)支持 selector,只订阅用到的切片

全局复杂状态见 状态管理

何时不该用 Context

  • 高频更新的数据(如鼠标位置、每秒多次的实时数据):Context 变化会让所有消费者重渲染,应改用状态库的 selector 机制或专门的发布订阅方案。
  • 只有一两层的 props 传递:直接传 props 更直观,Context 会增加一层间接性,非必要不用。
  • 组件库内部的局部状态共享(如 Tabs 组件内 Tab 与 TabPanel 的联动):Context 很适合,但要注意把它做成组件私有实现细节,不对外暴露。

最佳实践

  • 始终封装自定义 Hook(useTheme)而不是让消费方直接 useContext(ThemeContext),方便统一做空值校验与未来重构。
  • Provider 传递的 valueuseMemo 稳定引用,避免无意义的重渲染。
  • 按数据的更新频率拆分多个 Context,而不是一个「大而全」的 AppContext。
  • Context 默认值应该有意义(如 light 主题),或抛出清晰的错误提示未在 Provider 内使用。
  • 组件库/复用组件内部可以用 Context 传递跨组件状态(如表单字段间的联动),但应视为实现细节,避免污染公共 API。

常见坑

现象常见原因处理
消费方全部重渲染,即使只关心一个字段单一大 Context,value 是聚合对象拆分多个 Context 或改用状态库 selector
useTheme 报错「须在 Provider 内使用」组件树中未包裹对应 Provider检查 Provider 位置,确保覆盖使用该 Hook 的子树
Provider 每次渲染都导致子树重渲染valueuseMemo,每次都是新对象useMemo 包裹 value
Context 被当作「万能全局状态」滥用缺乏对状态库的评估,什么都往 Context 里塞复杂/高频状态迁移到专门的状态管理方案

延伸阅读

参考文献

以下链接在编写时均可正常访问:

Series

hooks

1 / 4