# React Context API 最佳实践
## 规则类型
代码审查 (REVIEW)
## 适用技术
React, TypeScript, JavaScript
## 问题描述
在 React 应用中使用 Context API 进行状态管理时,开发者经常面临以下挑战:
- Context 结构设计不合理导致过度渲染
- 缺乏类型安全保障
- 状态逻辑与 UI 逻辑混合
- 不必要的组件重渲染
- 难以测试和维护的 Context 结构
- 不清楚何时使用 Context 而非其他状态管理方案
- Provider 嵌套过深导致的"Provider Hell"
## 规则内容
使用 React Context API 进行状态管理应遵循以下最佳实践:
### 1. Context 的设计原则
**单一职责原则**:每个 Context 应该只负责一个功能领域的状态管理。
```tsx
// ❌ 不好的做法:一个 Context 管理所有状态
const AppContext = createContext({
user: null,
theme: 'light',
notifications: [],
cart: [],
// ... 更多不相关的状态
})
// ✅ 好的做法:分离关注点
const UserContext = createContext(null)
const ThemeContext = createContext('light')
const NotificationContext = createContext([])
const CartContext = createContext([])
```
**提供默认值**:始终为 Context 提供有意义的默认值,避免使用 `undefined`。
```tsx
// ❌ 不好的做法:没有默认值或使用 undefined
const UserContext = createContext(undefined)
// ✅ 好的做法:提供类型安全的默认值
interface UserContextType {
user: User | null;
login: (credentials: Credentials) => Promise<void>;
logout: () => void;
}
const defaultUserContext: UserContextType = {
user: null,
login: async () => {
console.warn('UserContext not initialized')
},
logout: () => {
console.warn('UserContext not initialized')
},
}
const UserContext = createContext<UserContextType>(defaultUserContext)
```
### 2. Context Provider 的实现
**创建自定义 Provider 组件**:封装状态逻辑,提供清晰的 API。
```tsx
// UserProvider.tsx
import { createContext, useContext, useState, ReactNode } from 'react'
interface User {
id: string
name: string
email: string
}
interface Credentials {
email: string
password: string
}
interface UserContextType {
user: User | null
login: (credentials: Credentials) => Promise<void>
logout: () => void
isLoading: boolean
error: string | null
}
const defaultUserContext: UserContextType = {
user: null,
login: async () => {},
logout: () => {},
isLoading: false,
error: null,
}
const UserContext = createContext<UserContextType>(defaultUserContext)
export function UserProvider({ children }: { children: ReactNode }) {
const [user, setUser] = useState<User | null>(null)
const [isLoading, setIsLoading] = useState(false)
const [error, setError] = useState<string | null>(null)
const login = async (credentials: Credentials) => {
setIsLoading(true)
setError(null)
try {
// 实际应用中,这里会调用 API
const response = await apiClient.login(credentials)
setUser(response.user)
} catch (err) {
setError(err instanceof Error ? err.message : 'Unknown error')
} finally {
setIsLoading(false)
}
}
const logout = () => {
// 实际应用中,这里会调用 API
apiClient.logout()
setUser(null)
}
const value = {
user,
login,
logout,
isLoading,
error,
}
return <UserContext.Provider value={value}>{children}</UserContext.Provider>
}
// 自定义 hook 简化使用
export function useUser() {
const context = useContext(UserContext)
if (context === undefined) {
throw new Error('useUser must be used within a UserProvider')
}
return context
}
```
**组合多个 Provider**:使用组合模式避免嵌套地狱。
```tsx
// AppProviders.tsx
export function AppProviders({ children }: { children: ReactNode }) {
return (
<ErrorBoundary>
<UserProvider>
<ThemeProvider>
<NotificationProvider>
<CartProvider>{children}</CartProvider>
</NotificationProvider>
</ThemeProvider>
</UserProvider>
</ErrorBoundary>
)
}
// App.tsx
function App() {
return (
<AppProviders>
<Router>
{/* 应用组件 */}
</Router>
</AppProviders>
)
}
```
### 3. 性能优化
**使用 Context 分割**:将频繁变化的状态与稳定的状态分开。
```tsx
// ❌ 不好的做法:将频繁变化的状态与稳定状态放在一起
const AppContext = createContext({
user: { /* 稳定的用户数据 */ },
notifications: [] // 频繁变化的通知数据
})
// ✅ 好的做法:分离频繁变化的状态
const UserContext = createContext(null) // 稳定状态
const NotificationContext = createContext([]) // 频繁变化状态
```
**使用 React.memo 和 useMemo**:避免不必要的重渲染。
```tsx
// ThemeProvider.tsx
export function ThemeProvider({ children }: { children: ReactNode }) {
const [theme, setTheme] = useState('light')
// 使用 useMemo 缓存 context 值,只在 theme 变化时更新
const value = useMemo(() => ({
theme,
setTheme
}), [theme])
return (
<ThemeContext.Provider value={value}>
{children}
</ThemeContext.Provider>
)
}
// 使用 React.memo 避免子组件不必要的重渲染
const ThemedButton = React.memo(function ThemedButton({ onClick, children }) {
const { theme } = useTheme()
return (
<button
onClick={onClick}
className={theme === 'dark' ? 'dark-button' : 'light-button'}
>
{children}
</button>
)
})
```
**使用 Context Selector 模式**:只订阅需要的状态部分。
```tsx
// 使用 useContextSelector 库或自定义实现
import { createContext, useReducer, useContext, useRef, useCallback } from 'react'
import { useContextSelector } from 'use-context-selector'
// 创建一个支持选择器的 Context
function createSelectorContext(initialState) {
const Context = createContext(initialState)
function Provider({ children }) {
const [state, dispatch] = useReducer(reducer, initialState)
const value = { state, dispatch }
return <Context.Provider value={value}>{children}</Context.Provider>
}
function useSelector(selector) {
const { state } = useContext(Context)
const selectedState = selector(state)
// 简化版实现,实际使用中应该使用 useContextSelector 或类似库
return selectedState
}
return { Provider, useSelector }
}
// 使用
const { Provider, useSelector } = createSelectorContext(initialState)
// 在组件中只订阅需要的状态
function UserProfile() {
// 只有 user.profile 变化时才会重渲染
const profile = useSelector(state => state.user.profile)
return <div>{profile.name}</div>
}
```
### 4. 与 TypeScript 结合使用
**为 Context 和 Provider 提供完整类型**:
```tsx
// 定义状态和操作的类型
interface Theme {
mode: 'light' | 'dark'
primaryColor: string
secondaryColor: string
}
interface ThemeContextType {
theme: Theme
setTheme: (theme: Theme) => void
toggleMode: () => void
}
// 创建带有类型的 Context
const ThemeContext = createContext<ThemeContextType | undefined>(undefined)
// Provider 实现
export function ThemeProvider({ children }: { children: ReactNode }) {
const [theme, setTheme] = useState<Theme>({
mode: 'light',
primaryColor: '#007bff',
secondaryColor: '#6c757d',
})
const toggleMode = useCallback(() => {
setTheme(prev => ({
...prev,
mode: prev.mode === 'light' ? 'dark' : 'light',
}))
}, [])
const value = useMemo(
() => ({ theme, setTheme, toggleMode }),
[theme, toggleMode]
)
return (
<ThemeContext.Provider value={value}>
{children}
</ThemeContext.Provider>
)
}
// 自定义 hook 带有类型检查
export function useTheme(): ThemeContextType {
const context = useContext(ThemeContext)
if (context === undefined) {
throw new Error('useTheme must be used within a ThemeProvider')
}
return context
}
```
**使用泛型创建可重用的 Context 工厂**:
```tsx
// 创建一个通用的 Context 工厂
function createCtx<T>() {
const Context = createContext<T | undefined>(undefined)
function useCtx() {
const context = useContext(Context)
if (context === undefined) {
throw new Error('useCtx must be used within a Provider')
}
return context
}
return [Context.Provider, useCtx] as const
}
// 使用工厂创建特定的 Context
interface CounterContextType {
count: number
increment: () => void
decrement: () => void
}
const [CounterProvider, useCounter] = createCtx<CounterContextType>()
// 实现 Provider
function CounterProviderWithValue({ children }: { children: ReactNode }) {
const [count, setCount] = useState(0)
const increment = useCallback(() => setCount(c => c + 1), [])
const decrement = useCallback(() => setCount(c => c - 1), [])
const value = useMemo(
() => ({ count, increment, decrement }),
[count, increment, decrement]
)
return <CounterProvider value={value}>{children}</CounterProvider>
}
```
### 5. 测试 Context
**为测试提供包装器**:
```tsx
// test-utils.tsx
import { render, RenderOptions } from '@testing-library/react'
import { ReactElement } from 'react'
import { ThemeProvider } from './ThemeProvider'
import { UserProvider } from './UserProvider'
// 创建一个包含所有 Provider 的测试包装器
function AllProviders({ children }: { children: React.ReactNode }) {
return (
<ThemeProvider>
<UserProvider>{children}</UserProvider>
</ThemeProvider>
)
}
// 自定义 render 方法
function customRender(
ui: ReactElement,
options?: Omit<RenderOptions, 'wrapper'>
) {
return render(ui, { wrapper: AllProviders, ...options })
}
export * from '@testing-library/react'
export { customRender as render }
```
**为特定测试提供模拟值**:
```tsx
// UserProfile.test.tsx
import { render, screen } from '@testing-library/react'
import { UserContext } from './UserProvider'
import UserProfile from './UserProfile'
test('displays user name when logged in', () => {
const mockUser = { id: '1', name: 'Test User', email: '
[email protected]' }
render(
<UserContext.Provider value={{ user: mockUser, login: jest.fn(), logout: jest.fn(), isLoading: false, error: null }}>
<UserProfile />
</UserContext.Provider>
)
expect(screen.getByText('Test User')).toBeInTheDocument()
})
test('displays login button when logged out', () => {
render(
<UserContext.Provider value={{ user: null, login: jest.fn(), logout: jest.fn(), isLoading: false, error: null }}>
<UserProfile />
</UserContext.Provider>
)
expect(screen.getByText('Log In')).toBeInTheDocument()
})
```
### 6. Context 与其他状态管理方案的对比
**何时使用 Context API**:
- 适用于:
- 全局主题、用户认证、语言偏好等应用级状态
- 中小型应用的状态管理
- 需要避免多层 props 传递的情况
- 状态变化频率较低的数据
- 不适用于:
- 频繁变化的状态(可能导致性能问题)
- 复杂的状态逻辑和操作
- 需要中间件、时间旅行调试等高级功能
- 大型应用的全局状态管理
**与其他方案的对比**:
| 特性 | Context API | Redux | MobX | Zustand | Jotai/Recoil |
|------|------------|-------|------|---------|--------------|
| 学习曲线 | 低 | 高 | 中 | 低 | 中 |
| 样板代码 | 少 | 多 | 少 | 少 | 少 |
| 性能优化 | 需手动优化 | 内置优化 | 自动优化 | 内置优化 | 内置优化 |
| 开发工具 | 有限 | 强大 | 良好 | 良好 | 良好 |
| 适用规模 | 小到中型 | 中到大型 | 各种规模 | 各种规模 | 各种规模 |
| TypeScript支持 | 良好 | 良好 | 良好 | 优秀 | 优秀 |
## 常见陷阱和避免方法
### 1. Context 过度使用
**问题**: 为每个小功能创建 Context,导致 Provider 嵌套过多。
**解决方案**:
- 只为真正需要全局共享的状态创建 Context
- 考虑使用组合组件和 props 传递替代简单场景
- 使用组合模式减少 Provider 嵌套
### 2. 忽略性能优化
**问题**: Context 值变化导致所有消费组件重新渲染。
**解决方案**:
- 使用 `useMemo` 缓存 Context 值
- 分离频繁变化的状态到单独的 Context
- 使用 `React.memo` 包装消费组件
- 考虑使用 Context Selector 模式
### 3. 过度依赖 Context
**问题**: 将所有状态放入 Context,包括只在局部使用的状态。
**解决方案**:
- 使用组件本地状态管理局部状态
- 只将真正需要跨多个组件共享的状态放入 Context
- 考虑使用 props 传递替代浅层组件树
### 4. 缺乏类型安全
**问题**: 未为 Context 提供类型定义,导致运行时错误。
**解决方案**:
- 使用 TypeScript 为 Context 和 Provider 提供完整类型
- 创建自定义 hooks 封装 Context 使用,提供类型检查
- 为 Context 提供有意义的默认值
### 5. 难以测试
**问题**: Context 使组件与特定 Provider 耦合,难以单独测试。
**解决方案**:
- 创建测试专用的 Provider 包装器
- 为测试提供模拟的 Context 值
- 使用依赖注入模式,允许在测试中覆盖依赖
## 例外情况
在以下情况下可以适当调整这些规则:
- 极小型应用可以使用单一 Context 管理所有状态
- 原型或演示项目可以简化 Context 结构
- 特定性能要求可能需要使用其他状态管理库
- 团队熟悉度和项目约定可能影响选择
## 相关规则
- React 组件设计模式
- React 性能优化最佳实践
- TypeScript 与 React 集成指南
- React 测试策略
## 参考资料
- [React Context 官方文档](https://reactjs.org/docs/context.html)
- [React Hooks 官方文档](https://reactjs.org/docs/hooks-reference.html)
- [Kent C. Dodds: How to use React Context effectively](https://kentcdodds.com/blog/how-to-use-react-context-effectively)
- [TypeScript React Cheatsheet](https://react-typescript-cheatsheet.netlify.app/)
- [React Context for State Management](https://blog.logrocket.com/react-context-api-deep-dive-examples/)