名称: react-context-patterns 用户可调用: false 描述: 在React Context模式用于状态管理时使用。当需要在组件树之间共享状态而避免prop drilling时使用。 允许工具:
- Bash
- Read
React Context模式
掌握React Context模式,以行业最佳实践构建高性能、可扩展的React应用。
理解Prop Drilling与Context
Prop drilling发生在您将props传递通过多个不需要它们的组件层时,仅仅为了到达深层嵌套的组件。
// Prop Drilling (避免)
function App() {
const [user, setUser] = useState<User | null>(null);
return <Layout user={user} setUser={setUser} />;
}
function Layout({ user, setUser }: Props) {
// Layout不使用user,只是传递下去
return <Sidebar user={user} setUser={setUser} />;
}
function Sidebar({ user, setUser }: Props) {
// Sidebar不使用user,只是传递下去
return <UserMenu user={user} setUser={setUser} />;
}
function UserMenu({ user, setUser }: Props) {
// 最终在这里使用
return <div>{user?.name}</div>;
}
Context通过提供一种在组件之间共享值的方式来解决这个问题,而无需显式通过每一层传递props:
// 使用Context (更好)
const UserContext = createContext<UserContextType | undefined>(undefined);
function App() {
const [user, setUser] = useState<User | null>(null);
return (
<UserContext.Provider value={{ user, setUser }}>
<Layout />
</UserContext.Provider>
);
}
function Layout() {
return <Sidebar />; // 不需要props
}
function Sidebar() {
return <UserMenu />; // 不需要props
}
function UserMenu() {
const { user } = useContext(UserContext);
return <div>{user?.name}</div>;
}
使用TypeScript创建和使用Context
import { createContext, useContext, useState, ReactNode } from 'react';
interface User {
id: string;
name: string;
email: string;
role: 'admin' | 'user';
}
interface AuthContextType {
user: User | null;
login: (email: string, password: string) => Promise<void>;
logout: () => void;
isAuthenticated: boolean;
isLoading: boolean;
}
const AuthContext = createContext<AuthContextType | undefined>(undefined);
export function AuthProvider({ children }: { children: ReactNode }) {
const [user, setUser] = useState<User | null>(null);
const [isLoading, setIsLoading] = useState(false);
const login = async (email: string, password: string) => {
setIsLoading(true);
try {
const user = await api.login(email, password);
setUser(user);
} catch (error) {
console.error('登录失败:', error);
throw error;
} finally {
setIsLoading(false);
}
};
const logout = () => {
setUser(null);
api.clearSession();
};
const value = {
user,
login,
logout,
isAuthenticated: user !== null,
isLoading
};
return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>;
}
export function useAuth() {
const context = useContext(AuthContext);
if (!context) {
throw new Error('useAuth必须在AuthProvider内使用');
}
return context;
}
使用useReducer管理复杂状态的Context
import { createContext, useContext, useReducer, ReactNode } from 'react';
interface CartItem {
id: string;
name: string;
price: number;
quantity: number;
}
interface State {
items: CartItem[];
total: number;
}
type Action =
| { type: 'ADD_ITEM'; payload: CartItem }
| { type: 'REMOVE_ITEM'; payload: string }
| { type: 'UPDATE_QUANTITY'; payload: { id: string; quantity: number } }
| { type: 'CLEAR_CART' };
const CartContext = createContext<{
state: State;
dispatch: React.Dispatch<Action>;
} | undefined>(undefined);
function cartReducer(state: State, action: Action): State {
switch (action.type) {
case 'ADD_ITEM': {
const existingItem = state.items.find(i => i.id === action.payload.id);
if (existingItem) {
return {
items: state.items.map(i =>
i.id === action.payload.id
? { ...i, quantity: i.quantity + action.payload.quantity }
: i
),
total: state.total + action.payload.price * action.payload.quantity
};
}
return {
items: [...state.items, action.payload],
total: state.total + action.payload.price * action.payload.quantity
};
}
case 'REMOVE_ITEM': {
const item = state.items.find(i => i.id === action.payload);
return {
items: state.items.filter(i => i.id !== action.payload),
total: state.total - (item ? item.price * item.quantity : 0)
};
}
case 'UPDATE_QUANTITY': {
const item = state.items.find(i => i.id === action.payload.id);
if (!item) return state;
const priceDiff = item.price * (action.payload.quantity - item.quantity);
return {
items: state.items.map(i =>
i.id === action.payload.id
? { ...i, quantity: action.payload.quantity }
: i
),
total: state.total + priceDiff
};
}
case 'CLEAR_CART':
return { items: [], total: 0 };
default:
return state;
}
}
export function CartProvider({ children }: { children: ReactNode }) {
const [state, dispatch] = useReducer(cartReducer, {
items: [],
total: 0
});
return (
<CartContext.Provider value={{ state, dispatch }}>
{children}
</CartContext.Provider>
);
}
export function useCart() {
const context = useContext(CartContext);
if (!context) throw new Error('useCart必须在CartProvider内使用');
return context;
}
// 辅助hook,包含操作
export function useCartActions() {
const { dispatch } = useCart();
return {
addItem: (item: CartItem) => dispatch({ type: 'ADD_ITEM', payload: item }),
removeItem: (id: string) => dispatch({ type: 'REMOVE_ITEM', payload: id }),
updateQuantity: (id: string, quantity: number) =>
dispatch({ type: 'UPDATE_QUANTITY', payload: { id, quantity } }),
clearCart: () => dispatch({ type: 'CLEAR_CART' })
};
}
多个Context组合
import { ReactNode } from 'react';
// 组合多个providers
export function AppProviders({ children }: { children: ReactNode }) {
return (
<AuthProvider>
<ThemeProvider>
<CartProvider>
<NotificationProvider>
{children}
</NotificationProvider>
</CartProvider>
</ThemeProvider>
</AuthProvider>
);
}
// 在主应用中使用
function App() {
return (
<AppProviders>
<Router />
</AppProviders>
);
}
性能优化:拆分Context
拆分读写操作,防止不必要的重新渲染:
import { createContext, useContext, useState, ReactNode, useMemo } from 'react';
// 分离读写contexts
const UserStateContext = createContext<User | null>(null);
const UserDispatchContext = createContext<{
setUser: (user: User | null) => void;
} | undefined>(undefined);
export function UserProvider({ children }: { children: ReactNode }) {
const [user, setUser] = useState<User | null>(null);
// 使用useMemo防止重新渲染
const dispatch = useMemo(() => ({ setUser }), []);
return (
<UserStateContext.Provider value={user}>
<UserDispatchContext.Provider value={dispatch}>
{children}
</UserDispatchContext.Provider>
</UserStateContext.Provider>
);
}
// 组件只在使用的状态变化时重新渲染
export function useUser() {
const context = useContext(UserStateContext);
return context; // 可以为null
}
export function useUserDispatch() {
const context = useContext(UserDispatchContext);
if (!context) {
throw new Error('useUserDispatch必须在UserProvider内使用');
}
return context;
}
使用useMemo保持值稳定的Context
import { createContext, useContext, useState, ReactNode, useMemo } from 'react';
interface ThemeContextType {
theme: 'light' | 'dark';
toggleTheme: () => void;
primaryColor: string;
secondaryColor: string;
}
const ThemeContext = createContext<ThemeContextType | undefined>(undefined);
export function ThemeProvider({ children }: { children: ReactNode }) {
const [theme, setTheme] = useState<'light' | 'dark'>('light');
// 使用useMemo防止不必要的重新渲染
const value = useMemo(() => ({
theme,
toggleTheme: () => setTheme(t => t === 'light' ? 'dark' : 'light'),
primaryColor: theme === 'light' ? '#000000' : '#ffffff',
secondaryColor: theme === 'light' ? '#666666' : '#cccccc'
}), [theme]);
return (
<ThemeContext.Provider value={value}>
{children}
</ThemeContext.Provider>
);
}
export function useTheme() {
const context = useContext(ThemeContext);
if (!context) throw new Error('useTheme必须在ThemeProvider内使用');
return context;
}
带有本地存储持久化的Context
import { createContext, useContext, useState, useEffect, ReactNode } from 'react';
interface Settings {
notifications: boolean;
language: string;
timezone: string;
}
const SettingsContext = createContext<{
settings: Settings;
updateSettings: (updates: Partial<Settings>) => void;
} | undefined>(undefined);
const defaultSettings: Settings = {
notifications: true,
language: 'en',
timezone: 'UTC'
};
export function SettingsProvider({ children }: { children: ReactNode }) {
const [settings, setSettings] = useState<Settings>(() => {
// 从localStorage初始化
const stored = localStorage.getItem('settings');
return stored ? JSON.parse(stored) : defaultSettings;
});
// 变化时持久化到localStorage
useEffect(() => {
localStorage.setItem('settings', JSON.stringify(settings));
}, [settings]);
const updateSettings = (updates: Partial<Settings>) => {
setSettings(prev => ({ ...prev, ...updates }));
};
const value = { settings, updateSettings };
return (
<SettingsContext.Provider value={value}>
{children}
</SettingsContext.Provider>
);
}
export function useSettings() {
const context = useContext(SettingsContext);
if (!context) {
throw new Error('useSettings必须在SettingsProvider内使用');
}
return context;
}
用于特性标志的Context
import { createContext, useContext, ReactNode } from 'react';
interface FeatureFlags {
newDashboard: boolean;
betaFeatures: boolean;
experimentalUI: boolean;
}
const FeatureFlagsContext = createContext<FeatureFlags | undefined>(undefined);
export function FeatureFlagsProvider({
children,
flags
}: {
children: ReactNode;
flags: FeatureFlags;
}) {
return (
<FeatureFlagsContext.Provider value={flags}>
{children}
</FeatureFlagsContext.Provider>
);
}
export function useFeatureFlags() {
const context = useContext(FeatureFlagsContext);
if (!context) {
throw new Error('useFeatureFlags必须在FeatureFlagsProvider内使用');
}
return context;
}
export function useFeatureFlag(flag: keyof FeatureFlags): boolean {
const flags = useFeatureFlags();
return flags[flag];
}
// 使用
function App() {
const flags = fetchFeatureFlags(); // 从API或配置获取
return (
<FeatureFlagsProvider flags={flags}>
<Router />
</FeatureFlagsProvider>
);
}
function Dashboard() {
const newDashboard = useFeatureFlag('newDashboard');
return newDashboard ? <NewDashboard /> : <OldDashboard />;
}
用于通知/弹窗系统的Context
import { createContext, useContext, useState, ReactNode, useCallback } from 'react';
interface Notification {
id: string;
type: 'success' | 'error' | 'info' | 'warning';
message: string;
duration?: number;
}
interface NotificationContextType {
notifications: Notification[];
addNotification: (notification: Omit<Notification, 'id'>) => void;
removeNotification: (id: string) => void;
}
const NotificationContext = createContext<NotificationContextType | undefined>(
undefined
);
export function NotificationProvider({ children }: { children: ReactNode }) {
const [notifications, setNotifications] = useState<Notification[]>([]);
const addNotification = useCallback(
(notification: Omit<Notification, 'id'>) => {
const id = Math.random().toString(36).substr(2, 9);
const newNotification = { ...notification, id };
setNotifications(prev => [...prev, newNotification]);
// 持续时间后自动移除
if (notification.duration !== 0) {
setTimeout(() => {
removeNotification(id);
}, notification.duration || 5000);
}
},
[]
);
const removeNotification = useCallback((id: string) => {
setNotifications(prev => prev.filter(n => n.id !== id));
}, []);
const value = { notifications, addNotification, removeNotification };
return (
<NotificationContext.Provider value={value}>
{children}
<NotificationContainer />
</NotificationContext.Provider>
);
}
export function useNotifications() {
const context = useContext(NotificationContext);
if (!context) {
throw new Error('useNotifications必须在NotificationProvider内使用');
}
return context;
}
function NotificationContainer() {
const { notifications, removeNotification } = useNotifications();
return (
<div className="notification-container">
{notifications.map(notification => (
<div
key={notification.id}
className={`notification notification-${notification.type}`}
onClick={() => removeNotification(notification.id)}
>
{notification.message}
</div>
))}
</div>
);
}
用于模态框管理的Context
import { createContext, useContext, useState, ReactNode, useCallback } from 'react';
interface ModalContextType {
isOpen: boolean;
modalContent: ReactNode | null;
openModal: (content: ReactNode) => void;
closeModal: () => void;
}
const ModalContext = createContext<ModalContextType | undefined>(undefined);
export function ModalProvider({ children }: { children: ReactNode }) {
const [isOpen, setIsOpen] = useState(false);
const [modalContent, setModalContent] = useState<ReactNode | null>(null);
const openModal = useCallback((content: ReactNode) => {
setModalContent(content);
setIsOpen(true);
}, []);
const closeModal = useCallback(() => {
setIsOpen(false);
// 延迟清除内容以支持动画
setTimeout(() => setModalContent(null), 300);
}, []);
const value = { isOpen, modalContent, openModal, closeModal };
return (
<ModalContext.Provider value={value}>
{children}
{isOpen && (
<div className="modal-overlay" onClick={closeModal}>
<div className="modal-content" onClick={e => e.stopPropagation()}>
{modalContent}
<button onClick={closeModal}>关闭</button>
</div>
</div>
)}
</ModalContext.Provider>
);
}
export function useModal() {
const context = useContext(ModalContext);
if (!context) {
throw new Error('useModal必须在ModalProvider内使用');
}
return context;
}
// 使用
function UserProfile() {
const { openModal } = useModal();
const handleEditProfile = () => {
openModal(<EditProfileForm />);
};
return <button onClick={handleEditProfile}>编辑个人资料</button>;
}
用于表单状态管理的Context
import { createContext, useContext, useState, ReactNode } from 'react';
interface FormData {
[key: string]: any;
}
interface FormContextType {
formData: FormData;
errors: Record<string, string>;
setFieldValue: (field: string, value: any) => void;
setFieldError: (field: string, error: string) => void;
clearErrors: () => void;
resetForm: () => void;
}
const FormContext = createContext<FormContextType | undefined>(undefined);
export function FormProvider({
children,
initialValues = {}
}: {
children: ReactNode;
initialValues?: FormData;
}) {
const [formData, setFormData] = useState<FormData>(initialValues);
const [errors, setErrors] = useState<Record<string, string>>({});
const setFieldValue = (field: string, value: any) => {
setFormData(prev => ({ ...prev, [field]: value }));
// 字段修改时清除错误
if (errors[field]) {
setErrors(prev => {
const newErrors = { ...prev };
delete newErrors[field];
return newErrors;
});
}
};
const setFieldError = (field: string, error: string) => {
setErrors(prev => ({ ...prev, [field]: error }));
};
const clearErrors = () => setErrors({});
const resetForm = () => {
setFormData(initialValues);
setErrors({});
};
const value = {
formData,
errors,
setFieldValue,
setFieldError,
clearErrors,
resetForm
};
return <FormContext.Provider value={value}>{children}</FormContext.Provider>;
}
export function useForm() {
const context = useContext(FormContext);
if (!context) {
throw new Error('useForm必须在FormProvider内使用');
}
return context;
}
// 使用
function LoginForm() {
return (
<FormProvider initialValues={{ email: '', password: '' }}>
<Form />
</FormProvider>
);
}
function Form() {
const { formData, errors, setFieldValue } = useForm();
return (
<form>
<input
type="email"
value={formData.email}
onChange={e => setFieldValue('email', e.target.value)}
/>
{errors.email && <span>{errors.email}</span>}
<input
type="password"
value={formData.password}
onChange={e => setFieldValue('password', e.target.value)}
/>
{errors.password && <span>{errors.password}</span>}
</form>
);
}
测试Context Providers
import { render, screen } from '@testing-library/react';
import { AuthProvider, useAuth } from './AuthContext';
function TestComponent() {
const { user, isAuthenticated } = useAuth();
return (
<div>
<div data-testid="authenticated">{isAuthenticated.toString()}</div>
<div data-testid="user">{user?.name || 'None'}</div>
</div>
);
}
describe('AuthProvider', () => {
it('提供认证状态', () => {
render(
<AuthProvider>
<TestComponent />
</AuthProvider>
);
expect(screen.getByTestId('authenticated')).toHaveTextContent('false');
expect(screen.getByTestId('user')).toHaveTextContent('None');
});
it('在provider外使用时抛出错误', () => {
// 抑制此测试的console.error
const spy = jest.spyOn(console, 'error').mockImplementation();
expect(() => {
render(<TestComponent />);
}).toThrow('useAuth必须在AuthProvider内使用');
spy.mockRestore();
});
});
何时使用此技能
在需要时使用react-context-patterns:
- 共享状态于多个组件之间,避免prop drilling
- 实现全局应用状态(如认证、主题等)
- 为复杂功能构建provider模式
- 创建具有共享状态的复合组件
- 管理深层嵌套组件通信
- 实现特定于特性的状态管理
- 构建可扩展的React应用
- 避免过度传递props
- 创建可重用的context模式
- 管理横切关注点(如通知、模态框等)
最佳实践
-
按关注点拆分contexts - 为认证、主题、购物车等创建单独的contexts。不要组合不相关的状态。
-
使用useMemo记忆context值 - 防止provider重新渲染时不必要的重新渲染。
-
提供自定义hooks - 始终创建像
useAuth()这样的自定义hook,而不是直接暴露useContext()。 -
在provider外抛出错误 - 确保context在正确的provider边界内使用。
-
使用TypeScript - 为context值定义适当的类型,以在编译时捕获错误。
-
保持值稳定 - 避免在每次渲染时创建新对象/函数。使用
useMemo和useCallback。 -
拆分读写contexts - 对于性能关键的应用,分离状态和dispatch contexts。
-
文档化context使用 - 清晰记录每个context提供什么以及何时使用它。
-
全面测试 - 为providers、自定义hooks和错误情况编写测试。
-
考虑替代方案 - 不要对所有内容使用Context。对于某些情况,本地状态、prop传递或状态管理库可能更好。
常见陷阱
-
创建太多contexts - Context地狱和prop drilling一样糟糕。将相关状态分组在一起。
-
不使用useMemo记忆值 - 如果值不被记忆,每个provider重新渲染都会导致所有消费者重新渲染。
-
使用context处理所有状态 - 本地状态更简单且性能更高,适用于组件特定状态。
-
忘记错误边界 - 在自定义hooks中始终检查context是否存在,以提供有用的错误消息。
-
不提供默认值 - 始终处理context可能不可用的情况。
-
过度使用性能 - Context导致所有消费者重新渲染。对于频繁变化的值,考虑替代方案。
-
不拆分操作 - 分离读写可以显著提高性能。
-
创建不稳定的值 - 在provider内联定义对象或函数会导致不必要的重新渲染。
-
用于高频更新 - Context未针对每秒变化多次的值进行优化。
-
不考虑组合 - 有时提升状态或使用组合模式比context更简单。