React Context 深入解析
大约 8 分钟约 2410 字
React Context 深入解析
什么是 React Context?
React Context 提供了一种跨组件层级传递数据的方式,解决了 React 中经典的 prop drilling(逐层透传)问题。当你需要在组件树中共享数据,而又不想通过每一层组件手动传递 props 时,Context 是 React 内置的解决方案。
典型的 Context 使用场景包括:
- 主题切换(Theme)— 亮色/暗色模式
- 用户认证(Auth)— 登录状态和用户信息
- 国际化(i18n)— 当前语言和翻译函数
- 全局配置(Config)— API 地址、功能开关
Context 适合传递全局的、低频变化的数据。对于高频变化的场景(如实时数据、输入框值),应考虑使用 Zustand 等状态管理库。
基础用法
创建和使用 Context
import { createContext, useContext, useState, useMemo } from 'react';
// 1. 定义 Context 的类型
interface ThemeContextType {
mode: 'light' | 'dark';
toggle: () => void;
colors: {
primary: string;
bg: string;
text: string;
border: string;
};
}
// 2. 创建 Context(null! 表示必须有 Provider)
const ThemeContext = createContext<ThemeContextType>(null!);
// 3. 创建 Provider 组件
function ThemeProvider({ children }: { children: React.ReactNode }) {
const [mode, setMode] = useState<'light' | 'dark'>('light');
const toggle = () => setMode((m) => (m === 'light' ? 'dark' : 'light'));
// useMemo 确保颜色对象引用稳定,避免不必要的重渲染
const colors = useMemo(
() =>
mode === 'light'
? { primary: '#1890ff', bg: '#ffffff', text: '#333333', border: '#e8e8e8' }
: { primary: '#177ddc', bg: '#1f1f1f', text: '#e8e8e8', border: '#434343' },
[mode]
);
// value 也需要 useMemo,因为 colors 已经 memo 了
const value = useMemo(() => ({ mode, toggle, colors }), [mode, toggle, colors]);
return <ThemeContext.Provider value={value}>{children}</ThemeContext.Provider>;
}
// 4. 创建自定义 Hook 消费 Context
function useTheme() {
const ctx = useContext(ThemeContext);
if (!ctx) {
throw new Error('useTheme must be used within a ThemeProvider');
}
return ctx;
}
// 5. 在组件中使用
function Header() {
const { mode, toggle, colors } = useTheme();
return (
<header style={{ background: colors.bg, color: colors.text, borderBottom: `1px solid ${colors.border}` }}>
<h1>My App</h1>
<button onClick={toggle}>切换到{mode === 'light' ? '暗色' : '亮色'}</button>
</header>
);
}
function App() {
return (
<ThemeProvider>
<Header />
<MainContent />
</ThemeProvider>
);
}Context 的默认值机制
// 默认值只在没有匹配的 Provider 时生效
const ConfigContext = createContext({
apiUrl: 'http://localhost:3000',
debug: false,
});
// 如果组件不在 ConfigProvider 内,会使用默认值
function SomeComponent() {
const config = useContext(ConfigContext); // 获取默认值
return <span>API: {config.apiUrl}</span>;
}
// 带有 Provider 时使用 Provider 的值
function App() {
const configValue = { apiUrl: 'https://api.example.com', debug: true };
return (
<ConfigContext.Provider value={configValue}>
<SomeComponent /> {/* 使用 configValue */}
</ConfigContext.Provider>
);
}性能优化 — Context 的重渲染问题
问题:值变化导致所有消费者重渲染
Context 的一个核心性能问题是:当 Provider 的 value 变化时,所有消费该 Context 的组件都会重新渲染,即使它们只使用了 value 的一部分。
// 问题示例 — 一个大 Context 导致不必要的重渲染
interface AppContextType {
user: User | null;
theme: ThemeType;
notifications: Notification[];
locale: string;
// ... 更多字段
}
const AppContext = createContext<AppContextType>(null!);
function AppProvider({ children }) {
const [user, setUser] = useState(null);
const [theme, setTheme] = useState('light');
const [notifications, setNotifications] = useState([]);
const [locale, setLocale] = useState('zh-CN');
// 当 notifications 变化时,即使用户头像组件只依赖 user,
// 也会因为 value 是新对象而重渲染
const value = { user, theme, notifications, locale, setUser, setTheme };
return <AppContext.Provider value={value}>{children}</AppContext.Provider>;
}
function UserAvatar() {
const { user } = useContext(AppContext);
// notifications 变化也会导致这个组件重渲染!
return <img src={user?.avatar} />;
}解决方案 1:拆分 Context
// 将大 Context 拆分为多个小 Context
const UserContext = createContext<UserState>(null!);
const ThemeContext = createContext<ThemeState>(null!);
const NotificationContext = createContext<NotificationState>(null!);
function AppProvider({ children }) {
return (
<UserProvider>
<ThemeProvider>
<NotificationProvider>
{children}
</NotificationProvider>
</ThemeProvider>
</UserProvider>
);
}
// 现在 UserAvatar 只在 UserContext 变化时重渲染
function UserAvatar() {
const { user } = useContext(UserContext);
return <img src={user?.avatar} />;
}解决方案 2:状态和 Dispatch 分离
// 对于有读写操作的状态,拆分为 State 和 Dispatch 两个 Context
interface AuthState {
user: User | null;
isAuthenticated: boolean;
loading: boolean;
}
interface AuthDispatch {
login: (username: string, password: string) => Promise<void>;
logout: () => void;
register: (data: RegisterData) => Promise<void>;
}
const AuthStateContext = createContext<AuthState>(null!);
const AuthDispatchContext = createContext<AuthDispatch>(null!);
function AuthProvider({ children }: { children: React.ReactNode }) {
const [user, setUser] = useState<User | null>(null);
const [loading, setLoading] = useState(false);
// Dispatch 函数用 useCallback 稳定引用
const login = useCallback(async (username: string, password: string) => {
setLoading(true);
try {
const res = await fetch('/api/login', {
method: 'POST',
body: JSON.stringify({ username, password }),
});
const data = await res.json();
setUser(data.user);
} finally {
setLoading(false);
}
}, []);
const logout = useCallback(() => setUser(null), []);
const stateValue = useMemo(
() => ({ user, isAuthenticated: !!user, loading }),
[user, loading]
);
const dispatchValue = useMemo(() => ({ login, logout, register }), [login, logout]);
return (
<AuthStateContext.Provider value={stateValue}>
<AuthDispatchContext.Provider value={dispatchValue}>
{children}
</AuthDispatchContext.Provider>
</AuthStateContext.Provider>
);
}
// 只需要读取状态
function UserMenu() {
const { user, isAuthenticated } = useContext(AuthStateContext);
// 只有 user/loading 变化时重渲染
}
// 只需要触发操作
function LoginForm() {
const { login } = useContext(AuthDispatchContext);
const { loading } = useContext(AuthStateContext);
// login 的引用是稳定的,不会因为状态变化而重渲染
}解决方案 3:使用 useReducer + Context
import { useReducer, createContext, useContext, useMemo } from 'react';
// 定义 Action 类型
type AuthAction =
| { type: 'LOGIN_START' }
| { type: 'LOGIN_SUCCESS'; payload: User }
| { type: 'LOGIN_FAILURE'; payload: string }
| { type: 'LOGOUT' };
interface AuthState {
user: User | null;
loading: boolean;
error: string | null;
}
function authReducer(state: AuthState, action: AuthAction): AuthState {
switch (action.type) {
case 'LOGIN_START':
return { ...state, loading: true, error: null };
case 'LOGIN_SUCCESS':
return { ...state, loading: false, user: action.payload };
case 'LOGIN_FAILURE':
return { ...state, loading: false, error: action.payload };
case 'LOGOUT':
return { ...state, user: null };
default:
return state;
}
}
const AuthContext = createContext<{
state: AuthState;
dispatch: React.Dispatch<AuthAction>;
}>(null!);
function AuthProvider({ children }) {
const [state, dispatch] = useReducer(authReducer, {
user: null,
loading: false,
error: null,
});
// dispatch 引用始终稳定,state 变化时 value 变化
const value = useMemo(() => ({ state, dispatch }), [state]);
return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>;
}实战案例:国际化 Context
// i18n/translations.ts
const translations = {
'zh-CN': {
common: { confirm: '确认', cancel: '取消', save: '保存', delete: '删除' },
nav: { home: '首页', settings: '设置', profile: '个人中心' },
},
'en-US': {
common: { confirm: 'Confirm', cancel: 'Cancel', save: 'Save', delete: 'Delete' },
nav: { home: 'Home', settings: 'Settings', profile: 'Profile' },
},
} as const;
type Locale = keyof typeof translations;
type TranslationKey = keyof typeof translations['zh-CN'];
// i18n/context.tsx
interface I18nContextType {
locale: Locale;
setLocale: (locale: Locale) => void;
t: (key: string) => string;
}
const I18nContext = createContext<I18nContextType>(null!);
function I18nProvider({ children }: { children: React.ReactNode }) {
const [locale, setLocale] = useState<Locale>('zh-CN');
const t = useCallback(
(key: string): string => {
const keys = key.split('.');
// @ts-ignore — 简化的嵌套取值
let result: any = translations[locale];
for (const k of keys) {
result = result?.[k];
}
return result ?? key; // 找不到翻译时返回 key
},
[locale]
);
const value = useMemo(() => ({ locale, setLocale, t }), [locale, setLocale, t]);
return <I18nContext.Provider value={value}>{children}</I18nContext.Provider>;
}
function useI18n() {
return useContext(I18nContext);
}
// 组件中使用
function ConfirmDialog() {
const { t } = useI18n();
return (
<div>
<button>{t('common.confirm')}</button>
<button>{t('common.cancel')}</button>
</div>
);
}实战案例:权限控制 Context
interface PermissionContextType {
permissions: string[];
hasPermission: (permission: string) => boolean;
hasAnyPermission: (...permissions: string[]) => boolean;
hasAllPermissions: (...permissions: string[]) => boolean;
}
const PermissionContext = createContext<PermissionContextType>(null!);
function PermissionProvider({ children }: { children: React.ReactNode }) {
const [permissions, setPermissions] = useState<string[]>([]);
useEffect(() => {
// 从 API 加载权限列表
fetch('/api/permissions')
.then((r) => r.json())
.then((data) => setPermissions(data.permissions));
}, []);
const hasPermission = useCallback(
(permission: string) => permissions.includes(permission),
[permissions]
);
const hasAnyPermission = useCallback(
(...perms: string[]) => perms.some((p) => permissions.includes(p)),
[permissions]
);
const hasAllPermissions = useCallback(
(...perms: string[]) => perms.every((p) => permissions.includes(p)),
[permissions]
);
const value = useMemo(
() => ({ permissions, hasPermission, hasAnyPermission, hasAllPermissions }),
[permissions, hasPermission, hasAnyPermission, hasAllPermissions]
);
return <PermissionContext.Provider value={value}>{children}</PermissionContext.Provider>;
}
// 权限守卫组件
function RequirePermission({ permission, children }: { permission: string; children: React.ReactNode }) {
const { hasPermission } = useContext(PermissionContext);
if (!hasPermission(permission)) {
return <div>无权限访问</div>;
}
return <>{children}</>;
}
// 使用
function AdminPanel() {
return (
<RequirePermission permission="admin:access">
<h1>管理面板</h1>
</RequirePermission>
);
}React 19 的 Context 新特性
use() Hook
React 19 引入了 use() Hook,可以在条件语句和循环中读取 Context(传统的 useContext 只能在组件顶层调用)。
// React 19 之前 — useContext 不能在条件中使用
function ConditionalComponent({ showTheme }) {
// Error: Hooks cannot be called conditionally
if (showTheme) {
const theme = useContext(ThemeContext); // 错误!
}
}
// React 19 — use() 可以在条件中使用
function ConditionalComponent({ showTheme }) {
if (showTheme) {
const theme = use(ThemeContext); // OK!
return <div style={{ background: theme.colors.bg }}>...</div>;
}
return <div>默认样式</div>;
}
// use() 也可以用于 Promise
function DataLoader() {
const data = use(fetchDataPromise);
return <div>{data}</div>;
}Context 与其他方案对比
| 方案 | 适用场景 | 重渲染控制 | 复杂度 |
|---|---|---|---|
| Props 透传 | 父子组件少量数据 | 精确(只有 props 变化的组件重渲染) | 低 |
| Context | 跨层级全局数据 | 粗粒度(value 变化所有消费者重渲染) | 中 |
| Zustand | 全局状态管理 | 精确(selector 控制) | 低 |
| Redux | 大型应用复杂状态 | 精确(selector + reselect) | 高 |
选型建议:
- 主题、语言、认证等全局配置 → Context
- 需要频繁更新的数据(如实时消息)→ Zustand
- 大型团队协作项目 → Redux Toolkit
常见陷阱
陷阱 1:Context value 每次渲染创建新对象
// Bad — value 每次都是新对象
function Provider({ children }) {
const [count, setCount] = useState(0);
return (
<MyContext.Provider value={{ count, setCount }}>
{children}
</MyContext.Provider>
);
}
// Good — useMemo 稳定引用
function Provider({ children }) {
const [count, setCount] = useState(0);
const value = useMemo(() => ({ count, setCount }), [count]);
return (
<MyContext.Provider value={value}>
{children}
</MyContext.Provider>
);
}陷阱 2:忘记包裹 Provider
// 组件报错:useAuth 返回 null!
function Dashboard() {
const { user } = useAuth(); // 返回默认值 null
// 期望 user.name 但 user 是 null → TypeError
}
// 解决:确保 Provider 在组件树的上层
function App() {
return (
<AuthProvider>
<Router>
<Dashboard /> {/* 现在可以正确获取 user */}
</Router>
</AuthProvider>
);
}陷阱 3:使用 Context 管理高频变化的数据
// Bad — 鼠标位置每帧变化,导致所有消费者重渲染
const MouseContext = createContext({ x: 0, y: 0 });
function App() {
const [pos, setPos] = useState({ x: 0, y: 0 });
useEffect(() => {
const handler = (e) => setPos({ x: e.clientX, y: e.clientY });
window.addEventListener('mousemove', handler);
return () => window.removeEventListener('mousemove', handler);
}, []);
return (
<MouseContext.Provider value={pos}>
<HeavyComponent /> {/* 每帧重渲染! */}
</MouseContext.Provider>
);
}
// Good — 使用 ref + 事件或 Zustand
// 方案 1:使用 CSS 变量
useEffect(() => {
const handler = (e) => {
document.documentElement.style.setProperty('--mouse-x', `${e.clientX}px`);
document.documentElement.style.setProperty('--mouse-y', `${e.clientY}px`);
};
window.addEventListener('mousemove', handler);
return () => window.removeEventListener('mousemove', handler);
}, []);测试 Context 组件
import { render, screen } from '@testing-library/react';
import { ThemeProvider, useTheme } from './ThemeProvider';
// 方式 1:用真正的 Provider 包裹
describe('Header with Theme', () => {
it('显示暗色模式', () => {
render(
<ThemeProvider initialMode="dark">
<Header />
</ThemeProvider>
);
expect(screen.getByText('切换到亮色')).toBeInTheDocument();
});
});
// 方式 2:Mock Context 值
const MockThemeContext = createContext<ThemeContextType>(null!);
function renderWithTheme(ui: React.ReactElement, value: ThemeContextType) {
return render(
<MockThemeContext.Provider value={value}>
{ui}
</MockThemeContext.Provider>
);
}
describe('Header', () => {
it('显示用户信息', () => {
renderWithTheme(
<Header />,
{ mode: 'light', toggle: vi.fn(), colors: { primary: '#1890ff', bg: '#fff', text: '#333', border: '#e8e8e8' } }
);
expect(screen.getByRole('button')).toHaveTextContent('切换到暗色');
});
});排错清单
- 确认组件在 Provider 树内部(React DevTools → Components 面板查看)
- 检查 Context value 是否每次渲染创建新对象(添加
console.log比较引用) - 使用 React DevTools 的 Profiler 确认重渲染范围
- 检查
useMemo的依赖数组是否完整 - 确认自定义 Hook 中有正确的错误提示(Provider 缺失时抛出明确错误)
最佳实践总结
- 只为真正需要跨层级共享的数据创建 Context,不要滥用
- 拆分 Context — 按关注点分离,避免大 Context 导致不必要的重渲染
- 用 useMemo 稳定 Context value — 避免每次渲染创建新对象
- 创建自定义 Hook — 封装
useContext并添加 Provider 缺失检查 - 状态和操作分离 — 读写不频繁的组件可以只订阅 dispatch Context
- 编写测试 — 为 Context Provider 和自定义 Hook 编写单元测试
