Context
Context 在组件树中跨层级传递数据,避免 props 逐层透传。见 组件、状态管理。
核心概念
没有 Context 时,跨越多层组件传递数据只能靠逐层透传 props("prop drilling")——中间的每一层组件即使自己不需要这份数据,也得帮忙往下传。Context 提供了一种「广播」机制:Provider 在树的某一层设置一个值,其下任意深度的组件都能用 useContext 直接读取,跳过中间层。
创建与使用
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 不仅传值,还要传更新方法:
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 提升性能
// 不佳:所有数据塞进一个 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 与全局状态库
| Context | Redux / Zustand 等 | |
|---|---|---|
| 内置 | 是 | 需安装 |
| 适用 | 主题、语言、鉴权用户信息等低频更新 | 复杂全局状态、时间旅行、中间件 |
| 性能 | 消费方在 value 变化时重渲染 | 库通常有更细粒度订阅 |
| 更新粒度 | 无法只订阅某个字段(除非手动拆多个 Context) | 支持 selector,只订阅用到的切片 |
全局复杂状态见 状态管理。
何时不该用 Context
- 高频更新的数据(如鼠标位置、每秒多次的实时数据):Context 变化会让所有消费者重渲染,应改用状态库的 selector 机制或专门的发布订阅方案。
- 只有一两层的 props 传递:直接传 props 更直观,Context 会增加一层间接性,非必要不用。
- 组件库内部的局部状态共享(如 Tabs 组件内 Tab 与 TabPanel 的联动):Context 很适合,但要注意把它做成组件私有实现细节,不对外暴露。
最佳实践
- 始终封装自定义 Hook(
useTheme)而不是让消费方直接useContext(ThemeContext),方便统一做空值校验与未来重构。 - Provider 传递的
value用useMemo稳定引用,避免无意义的重渲染。 - 按数据的更新频率拆分多个 Context,而不是一个「大而全」的 AppContext。
- Context 默认值应该有意义(如
light主题),或抛出清晰的错误提示未在 Provider 内使用。 - 组件库/复用组件内部可以用 Context 传递跨组件状态(如表单字段间的联动),但应视为实现细节,避免污染公共 API。
常见坑
| 现象 | 常见原因 | 处理 |
|---|---|---|
| 消费方全部重渲染,即使只关心一个字段 | 单一大 Context,value 是聚合对象 | 拆分多个 Context 或改用状态库 selector |
useTheme 报错「须在 Provider 内使用」 | 组件树中未包裹对应 Provider | 检查 Provider 位置,确保覆盖使用该 Hook 的子树 |
| Provider 每次渲染都导致子树重渲染 | value 未 useMemo,每次都是新对象 | 用 useMemo 包裹 value |
| Context 被当作「万能全局状态」滥用 | 缺乏对状态库的评估,什么都往 Context 里塞 | 复杂/高频状态迁移到专门的状态管理方案 |
延伸阅读
参考文献
以下链接在编写时均可正常访问:
相关文章
Effect 与副作用
useEffect 用于在组件渲染后执行与外部系统同步的逻辑(请求、订阅、手动改 DOM 等)。涵盖基本用法、依赖数组、适用/不适用场景、useLayoutEffect、最佳实践与常见坑。
Memo、Callback 与 Ref
本文介绍 useRef、useMemo、useCallback、memo 及自定义 Hook。见 Effect。
State 与 Reducer
Hooks 让函数组件拥有 state 与副作用能力。本文介绍 useState 与 useReducer。见 React 基础。
渲染与协调
React 将组件渲染为虚拟 DOM 树,再协调(reconciliation)到浏览器 DOM。理解该过程有助于解释 key、memo 与性能优化。涵盖渲染流程、Fiber、startTransition、Suspense、最佳实践与常见坑。
工程化
新建 React 项目时,官方与社区主流工具为 Vite;全栈场景常用 Next.js。涵盖 Vite + React、Create React App 现状、Next.js、目录结构、环境变量、部署、最佳实践与常见坑。
TypeScript
React 官方推荐 TypeScript。类型主要落在 Props、事件、Ref 与 Hooks 返回值上。见 组件。
Series
hooks
1 / 4