React状态管理
第四章 状态管理
React 应用的状态管理是构建复杂应用的关键。从简单的 useState 到全局状态管理解决方案,本章将深入探讨各种状态管理模式和最佳实践。
4.1 React 内置状态管理
4.1.1 useState Hook 深度解析
useState 基础与高级用法:
import React, { useState, useCallback, useEffect } from 'react';
// 基础 useState 使用
const BasicCounter = () => {
const [count, setCount] = useState(0);
const [message, setMessage] = useState('Hello React');
return (
<div>
<h2>Count: {count}</h2>
<button onClick={() => setCount(count + 1)}>Increment</button>
<button onClick={() => setCount(count - 1)}>Decrement</button>
<button onClick={() => setCount(0)}>Reset</button>
<input
value={message}
onChange={(e) => setMessage(e.target.value)}
placeholder="Type a message"
/>
<p>{message}</p>
</div>
);
};
// 函数式更新 - 推荐方式
const FunctionalCounter = () => {
const [count, setCount] = useState(0);
const increment = () => {
// 使用函数式更新,确保基于最新状态
setCount(prevCount => prevCount + 1);
};
const incrementByFive = () => {
// 多次函数式更新
setCount(prevCount => prevCount + 1);
setCount(prevCount => prevCount + 1);
setCount(prevCount => prevCount + 1);
setCount(prevCount => prevCount + 1);
setCount(prevCount => prevCount + 1);
};
return (
<div>
<h2>Count: {count}</h2>
<button onClick={increment}>Increment</button>
<button onClick={incrementByFive}>Add 5</button>
<button onClick={() => setCount(0)}>Reset</button>
</div>
);
};
// 复杂状态管理
const UserProfile = () => {
const [profile, setProfile] = useState({
name: '',
email: '',
age: 18,
preferences: {
theme: 'light',
notifications: true,
language: 'en'
}
});
const [errors, setErrors] = useState({});
const [isLoading, setIsLoading] = useState(false);
// 部分状态更新
const updateField = (field, value) => {
setProfile(prev => ({
...prev,
[field]: value
}));
};
// 深层嵌套状态更新
const updatePreference = (pref, value) => {
setProfile(prev => ({
...prev,
preferences: {
...prev.preferences,
[pref]: value
}
}));
};
// 批量更新状态
const updateProfile = (newData) => {
setProfile(prev => ({
...prev,
...newData
}));
};
// 表单验证
const validateField = (field, value) => {
const newErrors = { ...errors };
switch (field) {
case 'name':
if (!value.trim()) {
newErrors.name = 'Name is required';
} else if (value.length < 2) {
newErrors.name = 'Name must be at least 2 characters';
} else {
delete newErrors.name;
}
break;
case 'email':
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
if (!emailRegex.test(value)) {
newErrors.email = 'Invalid email format';
} else {
delete newErrors.email;
}
break;
case 'age':
if (value < 18 || value > 120) {
newErrors.age = 'Age must be between 18 and 120';
} else {
delete newErrors.age;
}
break;
}
setErrors(newErrors);
};
// 处理表单提交
const handleSubmit = async (e) => {
e.preventDefault();
setIsLoading(true);
try {
// 验证所有字段
Object.keys(profile).forEach(field => {
if (field !== 'preferences') {
validateField(field, profile[field]);
}
});
if (Object.keys(errors).length === 0) {
// 提交数据
const response = await fetch('/api/profile', {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(profile)
});
if (response.ok) {
console.log('Profile updated successfully');
}
}
} catch (error) {
console.error('Error updating profile:', error);
} finally {
setIsLoading(false);
}
};
return (
<form onSubmit={handleSubmit} className="profile-form">
<div className="form-group">
<label>Name:</label>
<input
type="text"
value={profile.name}
onChange={(e) => {
updateField('name', e.target.value);
validateField('name', e.target.value);
}}
className={errors.name ? 'error' : ''}
/>
{errors.name && <span className="error-message">{errors.name}</span>}
</div>
<div className="form-group">
<label>Email:</label>
<input
type="email"
value={profile.email}
onChange={(e) => {
updateField('email', e.target.value);
validateField('email', e.target.value);
}}
className={errors.email ? 'error' : ''}
/>
{errors.email && <span className="error-message">{errors.email}</span>}
</div>
<div className="form-group">
<label>Age:</label>
<input
type="number"
value={profile.age}
onChange={(e) => {
updateField('age', parseInt(e.target.value));
validateField('age', parseInt(e.target.value));
}}
className={errors.age ? 'error' : ''}
/>
{errors.age && <span className="error-message">{errors.age}</span>}
</div>
<div className="form-group">
<label>Theme:</label>
<select
value={profile.preferences.theme}
onChange={(e) => updatePreference('theme', e.target.value)}
>
<option value="light">Light</option>
<option value="dark">Dark</option>
<option value="auto">Auto</option>
</select>
</div>
<div className="form-group">
<label>
<input
type="checkbox"
checked={profile.preferences.notifications}
onChange={(e) => updatePreference('notifications', e.target.checked)}
/>
Enable Notifications
</label>
</div>
<div className="form-group">
<label>Language:</label>
<select
value={profile.preferences.language}
onChange={(e) => updatePreference('language', e.target.value)}
>
<option value="en">English</option>
<option value="zh">中文</option>
<option value="es">Español</option>
</select>
</div>
<button type="submit" disabled={isLoading || Object.keys(errors).length > 0}>
{isLoading ? 'Saving...' : 'Save Profile'}
</button>
<pre style={{ marginTop: '20px', background: '#f5f5f5', padding: '10px' }}>
{JSON.stringify(profile, null, 2)}
</pre>
</form>
);
};
useState 性能优化:
// 状态结构优化 - 避免不必要的状态更新
const OptimizedComponent = () => {
// ❌ 分离的状态可能导致多次渲染
const [name, setName] = useState('');
const [email, setEmail] = useState('');
const [age, setAge] = useState('');
const [isValid, setIsValid] = useState(false);
const [loading, setLoading] = useState(false);
// ✅ 相关状态组合
const [formData, setFormData] = useState({
name: '',
email: '',
age: ''
});
const [uiState, setUiState] = useState({
isValid: false,
loading: false,
errors: {}
});
// 使用 useReducer 处理复杂状态逻辑
const formReducer = (state, action) => {
switch (action.type) {
case 'SET_FIELD':
return {
...state,
formData: {
...state.formData,
[action.field]: action.value
}
};
case 'SET_LOADING':
return {
...state,
loading: action.loading
};
case 'SET_ERRORS':
return {
...state,
errors: action.errors,
isValid: Object.keys(action.errors).length === 0
};
case 'RESET_FORM':
return {
formData: { name: '', email: '', age: '' },
loading: false,
errors: {},
isValid: false
};
default:
return state;
}
};
const [formState, dispatch] = React.useReducer(formReducer, {
formData: { name: '', email: '', age: '' },
loading: false,
errors: {},
isValid: false
});
// 优化的字段更新
const updateField = useCallback((field, value) => {
dispatch({
type: 'SET_FIELD',
field,
value
});
}, []);
// 优化的验证逻辑
const validateForm = useCallback(() => {
const errors = {};
if (!formState.formData.name.trim()) {
errors.name = 'Name is required';
}
if (!formState.formData.email.trim()) {
errors.email = 'Email is required';
} else if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(formState.formData.email)) {
errors.email = 'Invalid email format';
}
if (!formState.formData.age || formState.formData.age < 18) {
errors.age = 'Must be at least 18 years old';
}
dispatch({
type: 'SET_ERRORS',
errors
});
return Object.keys(errors).length === 0;
}, [formState.formData]);
// 计算属性优化
const formIsValid = useMemo(() => {
const { name, email, age } = formState.formData;
return name.trim() && email.trim() && age >= 18;
}, [formState.formData]);
return (
<div>
<h2>Optimized Form</h2>
<div className="form-group">
<label>Name:</label>
<input
type="text"
value={formState.formData.name}
onChange={(e) => updateField('name', e.target.value)}
/>
{formState.errors.name && (
<span className="error">{formState.errors.name}</span>
)}
</div>
<div className="form-group">
<label>Email:</label>
<input
type="email"
value={formState.formData.email}
onChange={(e) => updateField('email', e.target.value)}
/>
{formState.errors.email && (
<span className="error">{formState.errors.email}</span>
)}
</div>
<div className="form-group">
<label>Age:</label>
<input
type="number"
value={formState.formData.age}
onChange={(e) => updateField('age', parseInt(e.target.value))}
/>
{formState.errors.age && (
<span className="error">{formState.errors.age}</span>
)}
</div>
<div className="form-actions">
<button
onClick={validateForm}
disabled={formState.loading}
>
{formState.loading ? 'Validating...' : 'Validate'}
</button>
<button onClick={() => dispatch({ type: 'RESET_FORM' })}>
Reset
</button>
</div>
<div className="form-status">
Form is valid: {formState.isValid ? '✅' : '❌'}
</div>
<pre>
Current state: {JSON.stringify(formState, null, 2)}
</pre>
</div>
);
};
4.1.2 useReducer 复杂状态管理
useReducer 基础与高级应用:
import React, { useReducer, useCallback, useEffect } from 'react';
// 购物车状态管理
const shoppingCartReducer = (state, action) => {
switch (action.type) {
case 'ADD_TO_CART': {
const { product, quantity = 1 } = action.payload;
const existingItem = state.items.find(item => item.id === product.id);
if (existingItem) {
return {
...state,
items: state.items.map(item =>
item.id === product.id
? { ...item, quantity: item.quantity + quantity }
: item
)
};
} else {
return {
...state,
items: [...state.items, { ...product, quantity }]
};
}
}
case 'REMOVE_FROM_CART': {
const { productId } = action.payload;
return {
...state,
items: state.items.filter(item => item.id !== productId)
};
}
case 'UPDATE_QUANTITY': {
const { productId, quantity } = action.payload;
if (quantity <= 0) {
return {
...state,
items: state.items.filter(item => item.id !== productId)
};
}
return {
...state,
items: state.items.map(item =>
item.id === productId ? { ...item, quantity } : item
)
};
}
case 'APPLY_DISCOUNT': {
const { discountCode } = action.payload;
let discountRate = 0;
switch (discountCode) {
case 'SAVE10':
discountRate = 0.1;
break;
case 'SAVE20':
discountRate = 0.2;
break;
case 'WELCOME':
discountRate = 0.15;
break;
default:
return {
...state,
discountError: 'Invalid discount code'
};
}
return {
...state,
discountCode,
discountRate,
discountError: null
};
}
case 'CLEAR_CART': {
return {
...state,
items: [],
discountCode: null,
discountRate: 0,
discountError: null
};
}
case 'SET_SHIPPING_METHOD': {
const { method, cost } = action.payload;
return {
...state,
shippingMethod: method,
shippingCost: cost
};
}
case 'SET_LOADING': {
return {
...state,
loading: action.loading
};
}
case 'SET_ERROR': {
return {
...state,
error: action.error
};
}
default:
return state;
}
};
// 购物车计算属性
const useCartCalculations = (cartState) => {
const {
items,
discountRate,
shippingCost
} = cartState;
const subtotal = React.useMemo(() => {
return items.reduce((total, item) => total + (item.price * item.quantity), 0);
}, [items]);
const discountAmount = React.useMemo(() => {
return subtotal * discountRate;
}, [subtotal, discountRate]);
const total = React.useMemo(() => {
return Math.max(0, subtotal - discountAmount + shippingCost);
}, [subtotal, discountAmount, shippingCost]);
const itemCount = React.useMemo(() => {
return items.reduce((total, item) => total + item.quantity, 0);
}, [items]);
return {
subtotal,
discountAmount,
total,
itemCount
};
};
const ShoppingCart = () => {
const [cartState, dispatch] = useReducer(shoppingCartReducer, {
items: [],
discountCode: null,
discountRate: 0,
discountError: null,
shippingMethod: 'standard',
shippingCost: 0,
loading: false,
error: null
});
const { subtotal, discountAmount, total, itemCount } = useCartCalculations(cartState);
// 购物车操作
const addToCart = useCallback((product, quantity = 1) => {
dispatch({
type: 'ADD_TO_CART',
payload: { product, quantity }
});
}, []);
const removeFromCart = useCallback((productId) => {
dispatch({
type: 'REMOVE_FROM_CART',
payload: { productId }
});
}, []);
const updateQuantity = useCallback((productId, quantity) => {
dispatch({
type: 'UPDATE_QUANTITY',
payload: { productId, quantity }
});
}, []);
const applyDiscount = useCallback((discountCode) => {
dispatch({
type: 'APPLY_DISCOUNT',
payload: { discountCode }
});
}, []);
const clearCart = useCallback(() => {
dispatch({ type: 'CLEAR_CART' });
}, []);
const setShippingMethod = useCallback((method, cost) => {
dispatch({
type: 'SET_SHIPPING_METHOD',
payload: { method, cost }
});
}, []);
// 模拟产品数据
const products = [
{ id: 1, name: 'Laptop', price: 999.99, category: 'Electronics' },
{ id: 2, name: 'Book', price: 29.99, category: 'Books' },
{ id: 3, name: 'Headphones', price: 199.99, category: 'Electronics' },
{ id: 4, name: 'Coffee Mug', price: 15.99, category: 'Home' },
{ id: 5, name: 'Desk Chair', price: 249.99, category: 'Furniture' }
];
return (
<div className="shopping-cart">
<h2>Shopping Cart</h2>
<div className="cart-container">
{/* 产品列表 */}
<div className="products-section">
<h3>Products</h3>
<div className="product-grid">
{products.map(product => (
<div key={product.id} className="product-card">
<h4>{product.name}</h4>
<p className="price">${product.price.toFixed(2)}</p>
<p className="category">{product.category}</p>
<button
onClick={() => addToCart(product, 1)}
className="add-to-cart-btn"
>
Add to Cart
</button>
</div>
))}
</div>
</div>
{/* 购物车内容 */}
<div className="cart-section">
<h3>Cart ({itemCount} items)</h3>
{cartState.items.length === 0 ? (
<p>Your cart is empty</p>
) : (
<>
<div className="cart-items">
{cartState.items.map(item => (
<div key={item.id} className="cart-item">
<div className="item-info">
<h4>{item.name}</h4>
<p className="price">${item.price.toFixed(2)}</p>
</div>
<div className="quantity-controls">
<button
onClick={() => updateQuantity(item.id, item.quantity - 1)}
>
-
</button>
<span>{item.quantity}</span>
<button
onClick={() => updateQuantity(item.id, item.quantity + 1)}
>
+
</button>
</div>
<div className="item-total">
${(item.price * item.quantity).toFixed(2)}
</div>
<button
onClick={() => removeFromCart(item.id)}
className="remove-btn"
>
×
</button>
</div>
))}
</div>
{/* 折扣应用 */}
<div className="discount-section">
<input
type="text"
placeholder="Enter discount code"
onKeyPress={(e) => {
if (e.key === 'Enter') {
applyDiscount(e.target.value);
e.target.value = '';
}
}}
/>
<button onClick={(e) => {
const input = e.target.previousElementSibling;
applyDiscount(input.value);
input.value = '';
}}>
Apply
</button>
{cartState.discountError && (
<span className="error">{cartState.discountError}</span>
)}
</div>
{/* 配送方式 */}
<div className="shipping-section">
<h4>Shipping Method</h4>
<div className="shipping-options">
<label>
<input
type="radio"
name="shipping"
checked={cartState.shippingMethod === 'standard'}
onChange={() => setShippingMethod('standard', 9.99)}
/>
Standard ($9.99)
</label>
<label>
<input
type="radio"
name="shipping"
checked={cartState.shippingMethod === 'express'}
onChange={() => setShippingMethod('express', 19.99)}
/>
Express ($19.99)
</label>
<label>
<input
type="radio"
name="shipping"
checked={cartState.shippingMethod === 'free'}
onChange={() => setShippingMethod('free', 0)}
/>
Free (5-7 days)
</label>
</div>
</div>
{/* 价格汇总 */}
<div className="price-summary">
<div className="price-row">
<span>Subtotal:</span>
<span>${subtotal.toFixed(2)}</span>
</div>
{discountAmount > 0 && (
<div className="price-row discount">
<span>Discount ({cartState.discountCode}):</span>
<span>-${discountAmount.toFixed(2)}</span>
</div>
)}
<div className="price-row">
<span>Shipping:</span>
<span>${cartState.shippingCost.toFixed(2)}</span>
</div>
<div className="price-row total">
<span>Total:</span>
<span>${total.toFixed(2)}</span>
</div>
</div>
<div className="cart-actions">
<button className="checkout-btn">
Proceed to Checkout
</button>
<button onClick={clearCart} className="clear-btn">
Clear Cart
</button>
</div>
</>
)}
</div>
</div>
</div>
);
};
// 任务管理器 - 复杂状态管理示例
const taskReducer = (state, action) => {
switch (action.type) {
case 'ADD_TASK': {
const { title, description, priority = 'medium', dueDate } = action.payload;
const newTask = {
id: Date.now(),
title,
description,
priority,
dueDate,
status: 'todo',
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString()
};
return {
...state,
tasks: [...state.tasks, newTask]
};
}
case 'UPDATE_TASK': {
const { taskId, updates } = action.payload;
return {
...state,
tasks: state.tasks.map(task =>
task.id === taskId
? { ...task, ...updates, updatedAt: new Date().toISOString() }
: task
)
};
}
case 'DELETE_TASK': {
const { taskId } = action.payload;
return {
...state,
tasks: state.tasks.filter(task => task.id !== taskId)
};
}
case 'SET_FILTER': {
return {
...state,
filter: action.filter
};
}
case 'SET_SEARCH': {
return {
...state,
searchTerm: action.searchTerm
};
}
case 'SET_SORT': {
return {
...state,
sortBy: action.sortBy
};
}
case 'SET_LOADING': {
return {
...state,
loading: action.loading
};
}
default:
return state;
}
};
const TaskManager = () => {
const [state, dispatch] = React.useReducer(taskReducer, {
tasks: [],
filter: 'all', // all, todo, in-progress, done
searchTerm: '',
sortBy: 'createdAt', // createdAt, dueDate, priority
loading: false
});
const { tasks, filter, searchTerm, sortBy } = state;
// 过滤和排序任务
const filteredAndSortedTasks = React.useMemo(() => {
let filtered = tasks;
// 应用状态过滤
if (filter !== 'all') {
filtered = filtered.filter(task => task.status === filter);
}
// 应用搜索过滤
if (searchTerm) {
filtered = filtered.filter(task =>
task.title.toLowerCase().includes(searchTerm.toLowerCase()) ||
task.description.toLowerCase().includes(searchTerm.toLowerCase())
);
}
// 应用排序
filtered.sort((a, b) => {
switch (sortBy) {
case 'dueDate':
return new Date(a.dueDate) - new Date(b.dueDate);
case 'priority':
const priorityOrder = { high: 3, medium: 2, low: 1 };
return priorityOrder[b.priority] - priorityOrder[a.priority];
case 'createdAt':
default:
return new Date(b.createdAt) - new Date(a.createdAt);
}
});
return filtered;
}, [tasks, filter, searchTerm, sortBy]);
const addTask = useCallback((taskData) => {
dispatch({
type: 'ADD_TASK',
payload: taskData
});
}, []);
const updateTask = useCallback((taskId, updates) => {
dispatch({
type: 'UPDATE_TASK',
payload: { taskId, updates }
});
}, []);
const deleteTask = useCallback((taskId) => {
dispatch({
type: 'DELETE_TASK',
payload: { taskId }
});
}, []);
const setFilter = useCallback((filter) => {
dispatch({ type: 'SET_FILTER', filter });
}, []);
const setSearchTerm = useCallback((searchTerm) => {
dispatch({ type: 'SET_SEARCH', searchTerm });
}, []);
const setSortBy = useCallback((sortBy) => {
dispatch({ type: 'SET_SORT', sortBy });
}, []);
return (
<div className="task-manager">
<h2>Task Manager</h2>
<TaskForm onAddTask={addTask} />
<div className="controls">
<input
type="text"
placeholder="Search tasks..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
/>
<select
value={filter}
onChange={(e) => setFilter(e.target.value)}
>
<option value="all">All Tasks</option>
<option value="todo">To Do</option>
<option value="in-progress">In Progress</option>
<option value="done">Done</option>
</select>
<select
value={sortBy}
onChange={(e) => setSortBy(e.target.value)}
>
<option value="createdAt">Created Date</option>
<option value="dueDate">Due Date</option>
<option value="priority">Priority</option>
</select>
</div>
<TaskList
tasks={filteredAndSortedTasks}
onUpdateTask={updateTask}
onDeleteTask={deleteTask}
/>
<div className="stats">
Total tasks: {filteredAndSortedTasks.length}
</div>
</div>
);
};
const TaskForm = ({ onAddTask }) => {
const [formData, setFormData] = useState({
title: '',
description: '',
priority: 'medium',
dueDate: ''
});
const handleSubmit = (e) => {
e.preventDefault();
if (formData.title.trim()) {
onAddTask(formData);
setFormData({
title: '',
description: '',
priority: 'medium',
dueDate: ''
});
}
};
return (
<form onSubmit={handleSubmit} className="task-form">
<input
type="text"
placeholder="Task title"
value={formData.title}
onChange={(e) => setFormData({ ...formData, title: e.target.value })}
required
/>
<textarea
placeholder="Task description"
value={formData.description}
onChange={(e) => setFormData({ ...formData, description: e.target.value })}
/>
<select
value={formData.priority}
onChange={(e) => setFormData({ ...formData, priority: e.target.value })}
>
<option value="low">Low Priority</option>
<option value="medium">Medium Priority</option>
<option value="high">High Priority</option>
</select>
<input
type="date"
value={formData.dueDate}
onChange={(e) => setFormData({ ...formData, dueDate: e.target.value })}
/>
<button type="submit">Add Task</button>
</form>
);
};
const TaskList = ({ tasks, onUpdateTask, onDeleteTask }) => {
return (
<div className="task-list">
{tasks.map(task => (
<TaskItem
key={task.id}
task={task}
onUpdate={onUpdateTask}
onDelete={onDeleteTask}
/>
))}
</div>
);
};
const TaskItem = ({ task, onUpdate, onDelete }) => {
const [isEditing, setIsEditing] = useState(false);
const [editData, setEditData] = useState({
title: task.title,
description: task.description,
priority: task.priority,
status: task.status
});
const handleSave = () => {
onUpdate(task.id, editData);
setIsEditing(false);
};
const handleStatusChange = (newStatus) => {
onUpdate(task.id, { status: newStatus });
};
const priorityColors = {
high: '#ff4444',
medium: '#ffaa00',
low: '#44aa44'
};
const statusOptions = ['todo', 'in-progress', 'done'];
return (
<div className={`task-item priority-${task.priority}`}>
<div className="task-header">
{isEditing ? (
<input
type="text"
value={editData.title}
onChange={(e) => setEditData({ ...editData, title: e.target.value })}
/>
) : (
<h4>{task.title}</h4>
)}
<span
className="priority-badge"
style={{ backgroundColor: priorityColors[task.priority] }}
>
{task.priority}
</span>
</div>
{isEditing ? (
<textarea
value={editData.description}
onChange={(e) => setEditData({ ...editData, description: e.target.value })}
/>
) : (
<p>{task.description}</p>
)}
<div className="task-meta">
<select
value={task.status}
onChange={(e) => handleStatusChange(e.target.value)}
>
{statusOptions.map(status => (
<option key={status} value={status}>
{status.replace('-', ' ')}
</option>
))}
</select>
{task.dueDate && (
<span className="due-date">
Due: {new Date(task.dueDate).toLocaleDateString()}
</span>
)}
</div>
<div className="task-actions">
{isEditing ? (
<>
<button onClick={handleSave}>Save</button>
<button onClick={() => {
setIsEditing(false);
setEditData({
title: task.title,
description: task.description,
priority: task.priority,
status: task.status
});
}}>
Cancel
</button>
</>
) : (
<>
<button onClick={() => setIsEditing(true)}>Edit</button>
<button onClick={() => onDelete(task.id)}>Delete</button>
</>
)}
</div>
</div>
);
};
通过这节的内容,你已经掌握了 React 内置状态管理的核心概念和高级应用技巧。接下来我们将探讨全局状态管理解决方案。
4.2 Context 全局状态管理
4.2.1 Context 基础与最佳实践
Context 设计模式:
import React, { createContext, useContext, useReducer, useMemo, useCallback } from 'react';
// 1. 创建专门的 Context
const AuthContext = createContext(null);
const ThemeContext = createContext(null);
const NotificationContext = createContext(null);
const UserPreferencesContext = createContext(null);
// 2. Auth Context 状态管理
const authReducer = (state, action) => {
switch (action.type) {
case 'LOGIN_START':
return {
...state,
loading: true,
error: null
};
case 'LOGIN_SUCCESS':
return {
...state,
user: action.payload.user,
token: action.payload.token,
isAuthenticated: true,
loading: false,
error: null
};
case 'LOGIN_FAILURE':
return {
...state,
loading: false,
error: action.payload.error,
isAuthenticated: false
};
case 'LOGOUT':
return {
...state,
user: null,
token: null,
isAuthenticated: false,
loading: false,
error: null
};
case 'UPDATE_USER':
return {
...state,
user: { ...state.user, ...action.payload }
};
case 'REFRESH_TOKEN':
return {
...state,
token: action.payload.token
};
default:
return state;
}
};
const AuthProvider = ({ children }) => {
const [state, dispatch] = useReducer(authReducer, {
user: null,
token: localStorage.getItem('token'),
isAuthenticated: false,
loading: false,
error: null
});
// 检查初始认证状态
React.useEffect(() => {
const token = localStorage.getItem('token');
if (token) {
// 验证 token 有效性
verifyToken(token);
}
}, []);
const verifyToken = async (token) => {
try {
const response = await fetch('/api/auth/verify', {
headers: { Authorization: `Bearer ${token}` }
});
if (response.ok) {
const userData = await response.json();
dispatch({
type: 'LOGIN_SUCCESS',
payload: { user: userData, token }
});
} else {
localStorage.removeItem('token');
}
} catch (error) {
localStorage.removeItem('token');
}
};
const login = async (credentials) => {
dispatch({ type: 'LOGIN_START' });
try {
const response = await fetch('/api/auth/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(credentials)
});
const data = await response.json();
if (response.ok) {
localStorage.setItem('token', data.token);
dispatch({
type: 'LOGIN_SUCCESS',
payload: { user: data.user, token: data.token }
});
return { success: true };
} else {
dispatch({
type: 'LOGIN_FAILURE',
payload: { error: data.message || 'Login failed' }
});
return { success: false, error: data.message };
}
} catch (error) {
dispatch({
type: 'LOGIN_FAILURE',
payload: { error: 'Network error' }
});
return { success: false, error: 'Network error' };
}
};
const logout = useCallback(() => {
localStorage.removeItem('token');
dispatch({ type: 'LOGOUT' });
}, []);
const updateUser = useCallback((userData) => {
dispatch({
type: 'UPDATE_USER',
payload: userData
});
}, []);
const refreshToken = useCallback(async () => {
try {
const response = await fetch('/api/auth/refresh', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${state.token}`
}
});
if (response.ok) {
const { token } = await response.json();
localStorage.setItem('token', token);
dispatch({
type: 'REFRESH_TOKEN',
payload: { token }
});
} else {
logout();
}
} catch (error) {
logout();
}
}, [state.token, logout]);
const value = useMemo(() => ({
...state,
login,
logout,
updateUser,
refreshToken
}), [state, login, logout, updateUser, refreshToken]);
return (
<AuthContext.Provider value={value}>
{children}
</AuthContext.Provider>
);
};
// 3. Theme Context
const themeReducer = (state, action) => {
switch (action.type) {
case 'SET_THEME':
return {
...state,
theme: action.theme,
customColors: action.customColors || state.customColors
};
case 'SET_CUSTOM_COLOR':
return {
...state,
customColors: {
...state.customColors,
[action.colorName]: action.colorValue
}
};
case 'SET_FONT_SIZE':
return {
...state,
fontSize: action.fontSize
};
case 'TOGGLE_DARK_MODE':
return {
...state,
darkMode: !state.darkMode,
theme: !state.darkMode ? 'dark' : 'light'
};
default:
return state;
}
};
const ThemeProvider = ({ children }) => {
const [state, dispatch] = React.useReducer(themeReducer, {
theme: localStorage.getItem('theme') || 'light',
darkMode: localStorage.getItem('theme') === 'dark',
fontSize: localStorage.getItem('fontSize') || 'medium',
customColors: JSON.parse(localStorage.getItem('customColors') || '{}')
});
// 保存主题设置到 localStorage
React.useEffect(() => {
localStorage.setItem('theme', state.theme);
localStorage.setItem('fontSize', state.fontSize);
localStorage.setItem('customColors', JSON.stringify(state.customColors));
document.body.className = state.theme;
document.body.style.fontSize = state.fontSize;
}, [state.theme, state.fontSize, state.customColors]);
const setTheme = useCallback((theme) => {
dispatch({ type: 'SET_THEME', theme, darkMode: theme === 'dark' });
}, []);
const setCustomColor = useCallback((colorName, colorValue) => {
dispatch({
type: 'SET_CUSTOM_COLOR',
colorName,
colorValue
});
}, []);
const setFontSize = useCallback((fontSize) => {
dispatch({ type: 'SET_FONT_SIZE', fontSize });
}, []);
const toggleDarkMode = useCallback(() => {
dispatch({ type: 'TOGGLE_DARK_MODE' });
}, []);
const value = useMemo(() => ({
...state,
setTheme,
setCustomColor,
setFontSize,
toggleDarkMode
}), [state, setTheme, setCustomColor, setFontSize, toggleDarkMode]);
return (
<ThemeContext.Provider value={value}>
{children}
</ThemeContext.Provider>
);
};
// 4. Notification Context
const notificationReducer = (state, action) => {
switch (action.type) {
case 'ADD_NOTIFICATION':
const notification = {
id: Date.now(),
...action.notification
};
return {
...state,
notifications: [...state.notifications, notification]
};
case 'REMOVE_NOTIFICATION':
return {
...state,
notifications: state.notifications.filter(
notification => notification.id !== action.id
)
};
case 'CLEAR_NOTIFICATIONS':
return {
...state,
notifications: []
};
default:
return state;
}
};
const NotificationProvider = ({ children }) => {
const [state, dispatch] = React.useReducer(notificationReducer, {
notifications: []
});
const addNotification = useCallback((notification) => {
dispatch({
type: 'ADD_NOTIFICATION',
notification: {
type: 'info',
duration: 5000,
...notification
}
});
// 自动移除通知
if (notification.duration !== 0) {
const duration = notification.duration || 5000;
setTimeout(() => {
dispatch({
type: 'REMOVE_NOTIFICATION',
id: Date.now()
});
}, duration);
}
}, []);
const removeNotification = useCallback((id) => {
dispatch({ type: 'REMOVE_NOTIFICATION', id });
}, []);
const clearNotifications = useCallback(() => {
dispatch({ type: 'CLEAR_NOTIFICATIONS' });
}, []);
const value = useMemo(() => ({
notifications: state.notifications,
addNotification,
removeNotification,
clearNotifications,
success: (message, options) => addNotification({ type: 'success', message, ...options }),
error: (message, options) => addNotification({ type: 'error', message, ...options }),
warning: (message, options) => addNotification({ type: 'warning', message, ...options }),
info: (message, options) => addNotification({ type: 'info', message, ...options })
}), [state.notifications, addNotification, removeNotification, clearNotifications]);
return (
<NotificationContext.Provider value={value}>
{children}
<NotificationContainer notifications={state.notifications} />
</NotificationContext.Provider>
);
};
const NotificationContainer = ({ notifications }) => {
const { removeNotification } = useNotifications();
return (
<div className="notification-container">
{notifications.map(notification => (
<NotificationItem
key={notification.id}
notification={notification}
onClose={() => removeNotification(notification.id)}
/>
))}
</div>
);
};
const NotificationItem = ({ notification, onClose }) => {
React.useEffect(() => {
if (notification.duration > 0) {
const timer = setTimeout(onClose, notification.duration);
return () => clearTimeout(timer);
}
}, [notification.duration, onClose]);
return (
<div className={`notification notification-${notification.type}`}>
<div className="notification-content">
{notification.icon && <span className="notification-icon">{notification.icon}</span>}
<span className="notification-message">{notification.message}</span>
</div>
<button onClick={onClose} className="notification-close">
×
</button>
</div>
);
};
// 5. 自定义 Hooks
const useAuth = () => {
const context = useContext(AuthContext);
if (!context) {
throw new Error('useAuth must be used within AuthProvider');
}
return context;
};
const useTheme = () => {
const context = useContext(ThemeContext);
if (!context) {
throw new Error('useTheme must be used within ThemeProvider');
}
return context;
};
const useNotifications = () => {
const context = useContext(NotificationContext);
if (!context) {
throw new Error('useNotifications must be used within NotificationProvider');
}
return context;
};
// 6. 组合 Provider
const AppProviders = ({ children }) => {
return (
<AuthProvider>
<ThemeProvider>
<NotificationProvider>
{children}
</NotificationProvider>
</ThemeProvider>
</AuthProvider>
);
};
// 7. 使用示例
const App = () => {
return (
<AppProviders>
<AppLayout />
</AppProviders>
);
};
const AppLayout = () => {
const { user, isAuthenticated, logout } = useAuth();
const { theme, darkMode, toggleDarkMode, fontSize, setFontSize } = useTheme();
const { success, error } = useNotifications();
return (
<div className={`app-layout ${theme}`} style={{ fontSize }}>
<header className="app-header">
<h1>My Application</h1>
<div className="header-controls">
<button onClick={toggleDarkMode}>
{darkMode ? '☀️' : '🌙'}
</button>
<select
value={fontSize}
onChange={(e) => setFontSize(e.target.value)}
>
<option value="small">Small</option>
<option value="medium">Medium</option>
<option value="large">Large</option>
</select>
{isAuthenticated ? (
<div className="user-info">
<span>Welcome, {user.name}!</span>
<button onClick={logout}>Logout</button>
</div>
) : (
<LoginModal />
)}
</div>
</header>
<main className="app-main">
<Dashboard />
</main>
</div>
);
};
const LoginModal = () => {
const { login, loading, error } = useAuth();
const { success } = useNotifications();
const [credentials, setCredentials] = useState({ username: '', password: '' });
const handleSubmit = async (e) => {
e.preventDefault();
const result = await login(credentials);
if (result.success) {
success('Login successful!');
} else {
error(result.error || 'Login failed');
}
};
return (
<div className="login-modal">
<form onSubmit={handleSubmit}>
<h2>Login</h2>
{error && <div className="error">{error}</div>}
<input
type="text"
placeholder="Username"
value={credentials.username}
onChange={(e) => setCredentials({ ...credentials, username: e.target.value })}
required
/>
<input
type="password"
placeholder="Password"
value={credentials.password}
onChange={(e) => setCredentials({ ...credentials, password: e.target.value })}
required
/>
<button type="submit" disabled={loading}>
{loading ? 'Logging in...' : 'Login'}
</button>
</form>
</div>
);
};
const Dashboard = () => {
const { user, isAuthenticated } = useAuth();
const { theme, setTheme, setCustomColor } = useTheme();
const { success, error, warning, info } = useNotifications();
if (!isAuthenticated) {
return <div>Please log in to access the dashboard.</div>;
}
return (
<div className="dashboard">
<h2>Welcome to Dashboard, {user.name}!</h2>
<div className="dashboard-section">
<h3>Theme Settings</h3>
<div className="theme-options">
<button onClick={() => setTheme('light')}>Light Theme</button>
<button onClick={() => setTheme('dark')}>Dark Theme</button>
<button onClick={() => setTheme('blue')}>Blue Theme</button>
</div>
<div className="color-customization">
<label>
Primary Color:
<input
type="color"
onChange={(e) => setCustomColor('primary', e.target.value)}
/>
</label>
<label>
Secondary Color:
<input
type="color"
onChange={(e) => setCustomColor('secondary', e.target.value)}
/>
</label>
</div>
</div>
<div className="dashboard-section">
<h3>Notification Testing</h3>
<div className="notification-tests">
<button onClick={() => success('Operation completed successfully!')}>
Success Notification
</button>
<button onClick={() => error('An error occurred!')}>
Error Notification
</button>
<button onClick={() => warning('Warning message!')}>
Warning Notification
</button>
<button onClick={() => info('Information message!')}>
Info Notification
</button>
</div>
</div>
<div className="dashboard-section">
<h3>User Profile</h3>
<UserProfile />
</div>
</div>
);
};
const UserProfile = () => {
const { user, updateUser } = useAuth();
const { success } = useNotifications();
const [isEditing, setIsEditing] = useState(false);
const [editData, setEditData] = useState({
name: user.name,
email: user.email
});
const handleSave = () => {
updateUser(editData);
setIsEditing(false);
success('Profile updated successfully!');
};
return (
<div className="user-profile">
{isEditing ? (
<div>
<input
type="text"
value={editData.name}
onChange={(e) => setEditData({ ...editData, name: e.target.value })}
/>
<input
type="email"
value={editData.email}
onChange={(e) => setEditData({ ...editData, email: e.target.value })}
/>
<button onClick={handleSave}>Save</button>
<button onClick={() => {
setIsEditing(false);
setEditData({ name: user.name, email: user.email });
}}>Cancel</button>
</div>
) : (
<div>
<p><strong>Name:</strong> {user.name}</p>
<p><strong>Email:</strong> {user.email}</p>
<button onClick={() => setIsEditing(true)}>Edit Profile</button>
</div>
)}
</div>
);
};
4.2.2 分层 Context 架构
领域驱动 Context 设计:
// 1. 领域 Context 分离
const ProductContext = createContext(null);
const CartContext = createContext(null);
const OrderContext = createContext(null);
const CustomerContext = createContext(null);
// 2. Product Context - 产品领域
const productReducer = (state, action) => {
switch (action.type) {
case 'SET_PRODUCTS':
return {
...state,
products: action.products,
loading: false,
error: null
};
case 'SET_CATEGORIES':
return {
...state,
categories: action.categories
};
case 'SET_FILTERS':
return {
...state,
filters: { ...state.filters, ...action.filters }
};
case 'SET_LOADING':
return {
...state,
loading: action.loading
};
case 'SET_ERROR':
return {
...state,
error: action.error,
loading: false
};
default:
return state;
}
};
const ProductProvider = ({ children }) => {
const [state, dispatch] = useReducer(productReducer, {
products: [],
categories: [],
filters: {
category: 'all',
priceRange: [0, 1000],
inStock: true,
sortBy: 'name'
},
loading: true,
error: null
});
// API 调用函数
const fetchProducts = useCallback(async (filters = {}) => {
dispatch({ type: 'SET_LOADING', loading: true });
try {
const queryParams = new URLSearchParams();
if (filters.category && filters.category !== 'all') {
queryParams.append('category', filters.category);
}
if (filters.minPrice) {
queryParams.append('minPrice', filters.minPrice);
}
if (filters.maxPrice) {
queryParams.append('maxPrice', filters.maxPrice);
}
if (filters.inStock) {
queryParams.append('inStock', 'true');
}
if (filters.sortBy) {
queryParams.append('sortBy', filters.sortBy);
}
const response = await fetch(`/api/products?${queryParams}`);
const products = await response.json();
dispatch({ type: 'SET_PRODUCTS', products });
} catch (error) {
dispatch({ type: 'SET_ERROR', error: error.message });
}
}, []);
const fetchCategories = useCallback(async () => {
try {
const response = await fetch('/api/categories');
const categories = await response.json();
dispatch({ type: 'SET_CATEGORIES', categories });
} catch (error) {
console.error('Failed to fetch categories:', error);
}
}, []);
const setFilters = useCallback((filters) => {
dispatch({ type: 'SET_FILTERS', filters });
fetchProducts({ ...state.filters, ...filters });
}, [state.filters, fetchProducts]);
const getProductById = useCallback((id) => {
return state.products.find(product => product.id === parseInt(id));
}, [state.products]);
// 初始化数据
React.useEffect(() => {
fetchCategories();
fetchProducts(state.filters);
}, []);
const value = useMemo(() => ({
...state,
fetchProducts,
fetchCategories,
setFilters,
getProductById
}), [state, fetchProducts, fetchCategories, setFilters, getProductById]);
return (
<ProductContext.Provider value={value}>
{children}
</ProductContext.Provider>
);
};
// 3. Cart Context - 购物车领域
const cartReducer = (state, action) => {
switch (action.type) {
case 'ADD_ITEM': {
const { product, quantity = 1 } = action.payload;
const existingItem = state.items.find(item => item.id === product.id);
if (existingItem) {
return {
...state,
items: state.items.map(item =>
item.id === product.id
? { ...item, quantity: item.quantity + quantity }
: item
)
};
} else {
return {
...state,
items: [...state.items, { ...product, quantity }]
};
}
}
case 'REMOVE_ITEM': {
return {
...state,
items: state.items.filter(item => item.id !== action.productId)
};
}
case 'UPDATE_QUANTITY': {
const { productId, quantity } = action.payload;
if (quantity <= 0) {
return {
...state,
items: state.items.filter(item => item.id !== productId)
};
}
return {
...state,
items: state.items.map(item =>
item.id === productId ? { ...item, quantity } : item
)
};
}
case 'CLEAR_CART':
return {
...state,
items: []
};
case 'APPLY_COUPON': {
const { code, discount } = action.payload;
return {
...state,
couponCode: code,
couponDiscount: discount
};
}
case 'REMOVE_COUPON':
return {
...state,
couponCode: null,
couponDiscount: 0
};
default:
return state;
}
};
const CartProvider = ({ children }) => {
const [state, dispatch] = useReducer(cartReducer, {
items: [],
couponCode: null,
couponDiscount: 0
});
// 计算属性
const cartCalculations = useMemo(() => {
const subtotal = state.items.reduce((total, item) => total + (item.price * item.quantity), 0);
const itemCount = state.items.reduce((total, item) => total + item.quantity, 0);
const discount = subtotal * (state.couponDiscount / 100);
const total = Math.max(0, subtotal - discount);
return {
subtotal,
itemCount,
discount,
total
};
}, [state.items, state.couponDiscount]);
const addItem = useCallback((product, quantity) => {
dispatch({ type: 'ADD_ITEM', payload: { product, quantity } });
}, []);
const removeItem = useCallback((productId) => {
dispatch({ type: 'REMOVE_ITEM', productId });
}, []);
const updateQuantity = useCallback((productId, quantity) => {
dispatch({ type: 'UPDATE_QUANTITY', payload: { productId, quantity } });
}, []);
const clearCart = useCallback(() => {
dispatch({ type: 'CLEAR_CART' });
}, []);
const applyCoupon = useCallback(async (code) => {
try {
const response = await fetch('/api/coupons/validate', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ code })
});
if (response.ok) {
const { discount } = await response.json();
dispatch({ type: 'APPLY_COUPON', payload: { code, discount } });
return { success: true, discount };
} else {
const { error } = await response.json();
return { success: false, error };
}
} catch (error) {
return { success: false, error: 'Failed to validate coupon' };
}
}, []);
const removeCoupon = useCallback(() => {
dispatch({ type: 'REMOVE_COUPON' });
}, []);
const value = useMemo(() => ({
...state,
...cartCalculations,
addItem,
removeItem,
updateQuantity,
clearCart,
applyCoupon,
removeCoupon
}), [state, cartCalculations, addItem, removeItem, updateQuantity, clearCart, applyCoupon, removeCoupon]);
return (
<CartContext.Provider value={value}>
{children}
</CartContext.Provider>
);
};
// 4. Order Context - 订单领域
const orderReducer = (state, action) => {
switch (action.type) {
case 'CREATE_ORDER':
return {
...state,
orders: [action.order, ...state.orders],
loading: false
};
case 'SET_ORDERS':
return {
...state,
orders: action.orders,
loading: false
};
case 'UPDATE_ORDER_STATUS':
return {
...state,
orders: state.orders.map(order =>
order.id === action.orderId
? { ...order, status: action.status, updatedAt: new Date().toISOString() }
: order
)
};
case 'SET_LOADING':
return {
...state,
loading: action.loading
};
case 'SET_ERROR':
return {
...state,
error: action.error,
loading: false
};
default:
return state;
}
};
const OrderProvider = ({ children }) => {
const [state, dispatch] = useReducer(orderReducer, {
orders: [],
loading: false,
error: null
});
const createOrder = useCallback(async (orderData) => {
dispatch({ type: 'SET_LOADING', loading: true });
try {
const response = await fetch('/api/orders', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(orderData)
});
if (response.ok) {
const order = await response.json();
dispatch({ type: 'CREATE_ORDER', order });
return { success: true, order };
} else {
const { error } = await response.json();
dispatch({ type: 'SET_ERROR', error });
return { success: false, error };
}
} catch (error) {
dispatch({ type: 'SET_ERROR', error: error.message });
return { success: false, error: error.message };
}
}, []);
const fetchOrders = useCallback(async (filters = {}) => {
dispatch({ type: 'SET_LOADING', loading: true });
try {
const queryParams = new URLSearchParams(filters);
const response = await fetch(`/api/orders?${queryParams}`);
const orders = await response.json();
dispatch({ type: 'SET_ORDERS', orders });
} catch (error) {
dispatch({ type: 'SET_ERROR', error: error.message });
}
}, []);
const updateOrderStatus = useCallback(async (orderId, status) => {
try {
const response = await fetch(`/api/orders/${orderId}/status`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ status })
});
if (response.ok) {
dispatch({ type: 'UPDATE_ORDER_STATUS', orderId, status });
return { success: true };
} else {
const { error } = await response.json();
return { success: false, error };
}
} catch (error) {
return { success: false, error: error.message };
}
}, []);
const value = useMemo(() => ({
...state,
createOrder,
fetchOrders,
updateOrderStatus
}), [state, createOrder, fetchOrders, updateOrderStatus]);
return (
<OrderContext.Provider value={value}>
{children}
</OrderContext.Provider>
);
};
// 5. 自定义 Hooks
const useProducts = () => {
const context = useContext(ProductContext);
if (!context) throw new Error('useProducts must be used within ProductProvider');
return context;
};
const useCart = () => {
const context = useContext(CartContext);
if (!context) throw new Error('useCart must be used within CartProvider');
return context;
};
const useOrders = () => {
const context = useContext(OrderContext);
if (!context) throw new Error('useOrders must be used within OrderProvider');
return context;
};
// 6. E-commerce 应用组合
const ECommerceApp = ({ children }) => {
return (
<ProductProvider>
<CartProvider>
<OrderProvider>
<NotificationProvider>
{children}
</NotificationProvider>
</OrderProvider>
</CartProvider>
</ProductProvider>
);
};
// 7. 使用示例
const ShoppingApp = () => {
const { products, categories, filters, setFilters } = useProducts();
const { items, total, addItem, removeItem, updateQuantity, applyCoupon } = useCart();
const { createOrder, orders } = useOrders();
const { success, error } = useNotifications();
const handleCheckout = async () => {
const orderData = {
items: items.map(item => ({
productId: item.id,
quantity: item.quantity,
price: item.price
})),
total,
status: 'pending'
};
const result = await createOrder(orderData);
if (result.success) {
success('Order created successfully!');
// 清空购物车等操作
} else {
error(result.error || 'Failed to create order');
}
};
return (
<div className="shopping-app">
<ProductFilters
categories={categories}
filters={filters}
setFilters={setFilters}
/>
<ProductGrid products={products} onAddToCart={addItem} />
<ShoppingCart
items={items}
total={total}
onRemoveItem={removeItem}
onUpdateQuantity={updateQuantity}
onApplyCoupon={applyCoupon}
onCheckout={handleCheckout}
/>
{orders.length > 0 && <OrderHistory orders={orders} />}
</div>
);
};
通过这些 Context 设计模式,你可以构建出清晰、可维护的分层状态管理架构,每个领域都有独立的管理逻辑和清晰的数据流。
4.3 Redux 状态管理
4.3.1 Redux 基础概念与架构
Redux 核心概念:
// 1. 安装依赖
// npm install @reduxjs/toolkit react-redux
// 2. Redux Toolkit 配置
import { configureStore, createSlice, createAsyncThunk } from '@reduxjs/toolkit';
// 3. 异步 Thunk - 用户数据获取
export const fetchUsers = createAsyncThunk(
'users/fetchUsers',
async (_, { rejectWithValue }) => {
try {
const response = await fetch('/api/users');
if (!response.ok) {
throw new Error('Failed to fetch users');
}
const data = await response.json();
return data;
} catch (error) {
return rejectWithValue(error.message);
}
}
);
export const createUser = createAsyncThunk(
'users/createUser',
async (userData, { rejectWithValue }) => {
try {
const response = await fetch('/api/users', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(userData)
});
if (!response.ok) {
throw new Error('Failed to create user');
}
const data = await response.json();
return data;
} catch (error) {
return rejectWithValue(error.message);
}
}
);
// 4. 用户 Slice
const usersSlice = createSlice({
name: 'users',
initialState: {
users: [],
currentUser: null,
loading: false,
error: null,
filters: {
search: '',
role: 'all',
status: 'all'
}
},
reducers: {
// 同步 actions
setCurrentUser: (state, action) => {
state.currentUser = action.payload;
},
clearCurrentUser: (state) => {
state.currentUser = null;
},
setFilters: (state, action) => {
state.filters = { ...state.filters, ...action.payload };
},
clearError: (state) => {
state.error = null;
},
// 本地更新用户
updateUserLocal: (state, action) => {
const { userId, updates } = action.payload;
const userIndex = state.users.findIndex(user => user.id === userId);
if (userIndex !== -1) {
state.users[userIndex] = { ...state.users[userIndex], ...updates };
}
},
// 本地删除用户
deleteUserLocal: (state, action) => {
const userId = action.payload;
state.users = state.users.filter(user => user.id !== userId);
}
},
// 异步 actions 的 reducers
extraReducers: (builder) => {
builder
// fetchUsers
.addCase(fetchUsers.pending, (state) => {
state.loading = true;
state.error = null;
})
.addCase(fetchUsers.fulfilled, (state, action) => {
state.loading = false;
state.users = action.payload;
})
.addCase(fetchUsers.rejected, (state, action) => {
state.loading = false;
state.error = action.payload;
})
// createUser
.addCase(createUser.pending, (state) => {
state.loading = true;
state.error = null;
})
.addCase(createUser.fulfilled, (state, action) => {
state.loading = false;
state.users.push(action.payload);
state.currentUser = action.payload;
})
.addCase(createUser.rejected, (state, action) => {
state.loading = false;
state.error = action.payload;
});
}
});
// 导出 actions
export const {
setCurrentUser,
clearCurrentUser,
setFilters,
clearError,
updateUserLocal,
deleteUserLocal
} = usersSlice.actions;
// 导出 reducer
export default usersSlice.reducer;
// 5. 认证 Slice
const authSlice = createSlice({
name: 'auth',
initialState: {
token: localStorage.getItem('token'),
isAuthenticated: false,
user: null,
loading: false,
error: null
},
reducers: {
loginStart: (state) => {
state.loading = true;
state.error = null;
},
loginSuccess: (state, action) => {
state.loading = false;
state.isAuthenticated = true;
state.token = action.payload.token;
state.user = action.payload.user;
localStorage.setItem('token', action.payload.token);
},
loginFailure: (state, action) => {
state.loading = false;
state.error = action.payload;
state.isAuthenticated = false;
state.token = null;
state.user = null;
localStorage.removeItem('token');
},
logout: (state) => {
state.isAuthenticated = false;
state.token = null;
state.user = null;
localStorage.removeItem('token');
},
updateUser: (state, action) => {
state.user = { ...state.user, ...action.payload };
},
clearError: (state) => {
state.error = null;
}
}
});
export const {
loginStart,
loginSuccess,
loginFailure,
logout,
updateUser,
clearError: clearAuthError
} = authSlice.actions;
export default authSlice.reducer;
// 6. 产品 Slice
const productsSlice = createSlice({
name: 'products',
initialState: {
products: [],
categories: [],
currentProduct: null,
loading: false,
error: null,
filters: {
category: 'all',
priceRange: [0, 1000],
inStock: true,
search: '',
sortBy: 'name'
},
pagination: {
page: 1,
limit: 20,
total: 0
}
},
reducers: {
setProducts: (state, action) => {
state.products = action.payload;
},
setCategories: (state, action) => {
state.categories = action.payload;
},
setCurrentProduct: (state, action) => {
state.currentProduct = action.payload;
},
setFilters: (state, action) => {
state.filters = { ...state.filters, ...action.payload };
},
setPagination: (state, action) => {
state.pagination = { ...state.pagination, ...action.payload };
},
setLoading: (state, action) => {
state.loading = action.payload;
},
setError: (state, action) => {
state.error = action.payload;
},
clearError: (state) => {
state.error = null;
}
}
});
export const {
setProducts,
setCategories,
setCurrentProduct,
setFilters: setProductFilters,
setPagination,
setLoading: setProductLoading,
setError: setProductError,
clearError: clearProductError
} = productsSlice.actions;
export default productsSlice.reducer;
// 7. 购物车 Slice
const cartSlice = createSlice({
name: 'cart',
initialState: {
items: [],
total: 0,
itemCount: 0,
loading: false,
error: null,
couponCode: null,
couponDiscount: 0
},
reducers: {
addItem: (state, action) => {
const { product, quantity = 1 } = action.payload;
const existingItem = state.items.find(item => item.id === product.id);
if (existingItem) {
existingItem.quantity += quantity;
} else {
state.items.push({ ...product, quantity });
}
// 重新计算总计
state.itemCount = state.items.reduce((total, item) => total + item.quantity, 0);
state.total = state.items.reduce((total, item) => total + (item.price * item.quantity), 0);
},
removeItem: (state, action) => {
const productId = action.payload;
state.items = state.items.filter(item => item.id !== productId);
// 重新计算总计
state.itemCount = state.items.reduce((total, item) => total + item.quantity, 0);
state.total = state.items.reduce((total, item) => total + (item.price * item.quantity), 0);
},
updateQuantity: (state, action) => {
const { productId, quantity } = action.payload;
if (quantity <= 0) {
state.items = state.items.filter(item => item.id !== productId);
} else {
const item = state.items.find(item => item.id === productId);
if (item) {
item.quantity = quantity;
}
}
// 重新计算总计
state.itemCount = state.items.reduce((total, item) => total + item.quantity, 0);
state.total = state.items.reduce((total, item) => total + (item.price * item.quantity), 0);
},
clearCart: (state) => {
state.items = [];
state.total = 0;
state.itemCount = 0;
state.couponCode = null;
state.couponDiscount = 0;
},
applyCoupon: (state, action) => {
const { code, discount } = action.payload;
state.couponCode = code;
state.couponDiscount = discount;
},
removeCoupon: (state) => {
state.couponCode = null;
state.couponDiscount = 0;
}
}
});
export const {
addItem,
removeItem,
updateQuantity,
clearCart,
applyCoupon,
removeCoupon
} = cartSlice.actions;
export default cartSlice.reducer;
// 8. 配置 Store
const store = configureStore({
reducer: {
users: usersSlice,
auth: authSlice,
products: productsSlice,
cart: cartSlice
},
middleware: (getDefaultMiddleware) =>
getDefaultMiddleware({
serializableCheck: {
// 忽略这些 action types 的序列化检查
ignoredActions: ['persist/PERSIST', 'persist/REHYDRATE'],
// 忽略这些字段的序列化检查
ignoredPaths: ['users.currentUser.lastLogin']
}
}),
devTools: process.env.NODE_ENV !== 'production'
});
export default store;
// 9. 根组件设置
import React from 'react';
import ReactDOM from 'react-dom/client';
import { Provider } from 'react-redux';
import { persistStore, persistReducer } from 'redux-persist';
import { PersistGate } from 'redux-persist/integration/react';
import storage from 'redux-persist/lib/storage';
// 持久化配置
const persistConfig = {
key: 'root',
storage,
whitelist: ['auth', 'cart'] // 只持久化 auth 和 cart
};
const persistedReducer = persistReducer(persistConfig, store);
// 在应用中使用
const App = () => {
return (
<Provider store={store}>
<PersistGate loading={<div>Loading...</div>} persistor={persistStore(store)}>
<MainApp />
</PersistGate>
</Provider>
);
};
4.3.2 React-Redux 集成使用
Hooks 与组件集成:
import React, { useEffect, useState } from 'react';
import { useSelector, useDispatch } from 'react-redux';
import {
fetchUsers,
createUser,
setCurrentUser,
setFilters,
updateUserLocal,
deleteUserLocal
} from './slices/usersSlice';
import {
loginStart,
loginSuccess,
loginFailure,
logout,
updateUser
} from './slices/authSlice';
import {
setProducts,
setProductFilters,
setCurrentProduct
} from './slices/productsSlice';
import {
addItem,
removeItem,
updateQuantity,
clearCart,
applyCoupon
} from './slices/cartSlice';
// 1. 用户管理组件
const UserManagement = () => {
const dispatch = useDispatch();
const {
users,
loading,
error,
filters,
currentUser
} = useSelector(state => state.users);
// 组件挂载时获取用户
useEffect(() => {
dispatch(fetchUsers());
}, [dispatch]);
// 过滤用户
const filteredUsers = React.useMemo(() => {
return users.filter(user => {
if (filters.search && !user.name.toLowerCase().includes(filters.search.toLowerCase())) {
return false;
}
if (filters.role !== 'all' && user.role !== filters.role) {
return false;
}
if (filters.status !== 'all' && user.status !== filters.status) {
return false;
}
return true;
});
}, [users, filters]);
const handleFilterChange = (filterType, value) => {
dispatch(setFilters({ [filterType]: value }));
};
const handleSelectUser = (user) => {
dispatch(setCurrentUser(user));
};
const handleCreateUser = (userData) => {
dispatch(createUser(userData));
};
const handleUpdateUser = (userId, updates) => {
dispatch(updateUserLocal({ userId, updates }));
};
const handleDeleteUser = (userId) => {
dispatch(deleteUserLocal(userId));
};
if (loading) {
return <div>Loading users...</div>;
}
if (error) {
return <div>Error: {error}</div>;
}
return (
<div className="user-management">
<h2>User Management</h2>
<UserFilters
filters={filters}
onFilterChange={handleFilterChange}
/>
<CreateUserForm onSubmit={handleCreateUser} />
<UserList
users={filteredUsers}
onSelect={handleSelectUser}
onUpdate={handleUpdateUser}
onDelete={handleDeleteUser}
selectedUserId={currentUser?.id}
/>
{currentUser && (
<UserDetails
user={currentUser}
onUpdate={handleUpdateUser}
/>
)}
</div>
);
};
const UserFilters = ({ filters, onFilterChange }) => {
return (
<div className="user-filters">
<input
type="text"
placeholder="Search users..."
value={filters.search}
onChange={(e) => onFilterChange('search', e.target.value)}
/>
<select
value={filters.role}
onChange={(e) => onFilterChange('role', e.target.value)}
>
<option value="all">All Roles</option>
<option value="admin">Admin</option>
<option value="user">User</option>
<option value="moderator">Moderator</option>
</select>
<select
value={filters.status}
onChange={(e) => onFilterChange('status', e.target.value)}
>
<option value="all">All Status</option>
<option value="active">Active</option>
<option value="inactive">Inactive</option>
<option value="suspended">Suspended</option>
</select>
</div>
);
};
const CreateUserForm = ({ onSubmit }) => {
const [formData, setFormData] = useState({
name: '',
email: '',
role: 'user',
status: 'active'
});
const handleSubmit = (e) => {
e.preventDefault();
onSubmit(formData);
setFormData({ name: '', email: '', role: 'user', status: 'active' });
};
return (
<form onSubmit={handleSubmit} className="create-user-form">
<h3>Create New User</h3>
<input
type="text"
placeholder="Name"
value={formData.name}
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
required
/>
<input
type="email"
placeholder="Email"
value={formData.email}
onChange={(e) => setFormData({ ...formData, email: e.target.value })}
required
/>
<select
value={formData.role}
onChange={(e) => setFormData({ ...formData, role: e.target.value })}
>
<option value="user">User</option>
<option value="moderator">Moderator</option>
<option value="admin">Admin</option>
</select>
<select
value={formData.status}
onChange={(e) => setFormData({ ...formData, status: e.target.value })}
>
<option value="active">Active</option>
<option value="inactive">Inactive</option>
</select>
<button type="submit">Create User</button>
</form>
);
};
const UserList = ({ users, onSelect, onUpdate, onDelete, selectedUserId }) => {
const [editingUserId, setEditingUserId] = useState(null);
const [editData, setEditData] = useState({});
const handleEdit = (user) => {
setEditingUserId(user.id);
setEditData({ role: user.role, status: user.status });
};
const handleSave = (userId) => {
onUpdate(userId, editData);
setEditingUserId(null);
setEditData({});
};
const handleCancel = () => {
setEditingUserId(null);
setEditData({});
};
return (
<div className="user-list">
<h3>Users ({users.length})</h3>
<table>
<thead>
<tr>
<th>Name</th>
<th>Email</th>
<th>Role</th>
<th>Status</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
{users.map(user => (
<tr
key={user.id}
className={user.id === selectedUserId ? 'selected' : ''}
onClick={() => onSelect(user)}
>
<td>{user.name}</td>
<td>{user.email}</td>
<td>
{editingUserId === user.id ? (
<select
value={editData.role}
onChange={(e) => setEditData({ ...editData, role: e.target.value })}
onClick={(e) => e.stopPropagation()}
>
<option value="user">User</option>
<option value="moderator">Moderator</option>
<option value="admin">Admin</option>
</select>
) : (
<span className={`role ${user.role}`}>{user.role}</span>
)}
</td>
<td>
{editingUserId === user.id ? (
<select
value={editData.status}
onChange={(e) => setEditData({ ...editData, status: e.target.value })}
onClick={(e) => e.stopPropagation()}
>
<option value="active">Active</option>
<option value="inactive">Inactive</option>
<option value="suspended">Suspended</option>
</select>
) : (
<span className={`status ${user.status}`}>{user.status}</span>
)}
</td>
<td>
{editingUserId === user.id ? (
<div onClick={(e) => e.stopPropagation()}>
<button onClick={() => handleSave(user.id)}>Save</button>
<button onClick={handleCancel}>Cancel</button>
</div>
) : (
<div>
<button onClick={(e) => {
e.stopPropagation();
handleEdit(user);
}}>Edit</button>
<button onClick={(e) => {
e.stopPropagation();
onDelete(user.id);
}}>Delete</button>
</div>
)}
</td>
</tr>
))}
</tbody>
</table>
</div>
);
};
const UserDetails = ({ user, onUpdate }) => {
const [isEditing, setIsEditing] = useState(false);
const [editData, setEditData] = useState({});
const handleEdit = () => {
setIsEditing(true);
setEditData({
name: user.name,
email: user.email,
bio: user.bio || ''
});
};
const handleSave = () => {
onUpdate(user.id, editData);
setIsEditing(false);
};
const handleCancel = () => {
setIsEditing(false);
setEditData({});
};
return (
<div className="user-details">
<h3>User Details</h3>
{isEditing ? (
<div>
<input
type="text"
value={editData.name}
onChange={(e) => setEditData({ ...editData, name: e.target.value })}
/>
<input
type="email"
value={editData.email}
onChange={(e) => setEditData({ ...editData, email: e.target.value })}
/>
<textarea
value={editData.bio}
onChange={(e) => setEditData({ ...editData, bio: e.target.value })}
placeholder="Bio"
/>
<button onClick={handleSave}>Save</button>
<button onClick={handleCancel}>Cancel</button>
</div>
) : (
<div>
<p><strong>Name:</strong> {user.name}</p>
<p><strong>Email:</strong> {user.email}</p>
<p><strong>Role:</strong> {user.role}</p>
<p><strong>Status:</strong> {user.status}</p>
<p><strong>Bio:</strong> {user.bio || 'No bio provided'}</p>
<button onClick={handleEdit}>Edit</button>
</div>
)}
</div>
);
};
// 2. 产品和购物车组件
const ShoppingApp = () => {
const dispatch = useDispatch();
const { products, filters, loading } = useSelector(state => state.products);
const { items, total, itemCount } = useSelector(state => state.cart);
useEffect(() => {
// 模拟获取产品数据
const mockProducts = [
{ id: 1, name: 'Laptop', price: 999.99, category: 'Electronics', inStock: true },
{ id: 2, name: 'Book', price: 29.99, category: 'Books', inStock: true },
{ id: 3, name: 'Headphones', price: 199.99, category: 'Electronics', inStock: false },
{ id: 4, name: 'Coffee Mug', price: 15.99, category: 'Home', inStock: true }
];
dispatch(setProducts(mockProducts));
}, [dispatch]);
const handleAddToCart = (product) => {
dispatch(addItem({ product }));
};
const handleRemoveFromCart = (productId) => {
dispatch(removeItem(productId));
};
const handleUpdateQuantity = (productId, quantity) => {
dispatch(updateQuantity({ productId, quantity }));
};
const handleApplyCoupon = (code) => {
// 模拟优惠券验证
const coupons = {
'SAVE10': 10,
'SAVE20': 20,
'WELCOME': 15
};
const discount = coupons[code];
if (discount) {
dispatch(applyCoupon({ code, discount }));
}
};
return (
<div className="shopping-app">
<ProductFilters
filters={filters}
onChange={(filters) => dispatch(setProductFilters(filters))}
/>
{loading ? (
<div>Loading products...</div>
) : (
<ProductGrid
products={products}
onAddToCart={handleAddToCart}
/>
)}
<ShoppingCart
items={items}
total={total}
itemCount={itemCount}
onRemoveItem={handleRemoveFromCart}
onUpdateQuantity={handleUpdateQuantity}
onApplyCoupon={handleApplyCoupon}
/>
</div>
);
};
const ProductFilters = ({ filters, onChange }) => {
return (
<div className="product-filters">
<input
type="text"
placeholder="Search products..."
value={filters.search}
onChange={(e) => onChange({ search: e.target.value })}
/>
<select
value={filters.category}
onChange={(e) => onChange({ category: e.target.value })}
>
<option value="all">All Categories</option>
<option value="Electronics">Electronics</option>
<option value="Books">Books</option>
<option value="Home">Home</option>
</select>
<select
value={filters.sortBy}
onChange={(e) => onChange({ sortBy: e.target.value })}
>
<option value="name">Name</option>
<option value="price">Price</option>
<option value="category">Category</option>
</select>
</div>
);
};
const ProductGrid = ({ products, onAddToCart }) => {
return (
<div className="product-grid">
{products.map(product => (
<ProductCard
key={product.id}
product={product}
onAddToCart={onAddToCart}
/>
))}
</div>
);
};
const ProductCard = ({ product, onAddToCart }) => {
return (
<div className="product-card">
<h4>{product.name}</h4>
<p className="category">{product.category}</p>
<p className="price">${product.price.toFixed(2)}</p>
<p className="stock">
{product.inStock ? 'In Stock' : 'Out of Stock'}
</p>
<button
onClick={() => onAddToCart(product)}
disabled={!product.inStock}
>
{product.inStock ? 'Add to Cart' : 'Out of Stock'}
</button>
</div>
);
};
const ShoppingCart = ({
items,
total,
itemCount,
onRemoveItem,
onUpdateQuantity,
onApplyCoupon
}) => {
const [couponCode, setCouponCode] = useState('');
return (
<div className="shopping-cart">
<h3>Shopping Cart ({itemCount} items)</h3>
{items.length === 0 ? (
<p>Your cart is empty</p>
) : (
<>
<div className="cart-items">
{items.map(item => (
<CartItem
key={item.id}
item={item}
onRemove={onRemoveItem}
onUpdateQuantity={onUpdateQuantity}
/>
))}
</div>
<div className="coupon-section">
<input
type="text"
placeholder="Enter coupon code"
value={couponCode}
onChange={(e) => setCouponCode(e.target.value)}
/>
<button onClick={() => onApplyCoupon(couponCode)}>
Apply Coupon
</button>
</div>
<div className="cart-summary">
<div className="total">Total: ${total.toFixed(2)}</div>
<button className="checkout-btn">Checkout</button>
</div>
</>
)}
</div>
);
};
const CartItem = ({ item, onRemove, onUpdateQuantity }) => {
return (
<div className="cart-item">
<div className="item-info">
<h4>{item.name}</h4>
<p className="price">${item.price.toFixed(2)}</p>
</div>
<div className="quantity-controls">
<button
onClick={() => onUpdateQuantity(item.id, item.quantity - 1)}
>
-
</button>
<span>{item.quantity}</span>
<button
onClick={() => onUpdateQuantity(item.id, item.quantity + 1)}
>
+
</button>
</div>
<div className="item-total">
${(item.price * item.quantity).toFixed(2)}
</div>
<button
onClick={() => onRemove(item.id)}
className="remove-btn"
>
×
</button>
</div>
);
};
通过 Redux Toolkit 和 React-Redux 的集成,你可以构建出强大、可预测的状态管理系统,同时享受现代化的开发体验和强大的开发者工具支持。
4.4 状态管理最佳实践与模式
4.4.1 状态设计原则
单一数据源与不可变性:
// 1. 状态结构设计原则
// ✅ 好的状态结构 - 扁平化、规范化
const goodStateStructure = {
entities: {
users: {
byId: {
1: { id: 1, name: 'Alice', email: 'alice@example.com' },
2: { id: 2, name: 'Bob', email: 'bob@example.com' }
},
allIds: [1, 2]
},
posts: {
byId: {
101: { id: 101, title: 'Hello', authorId: 1 },
102: { id: 102, title: 'World', authorId: 2 }
},
allIds: [101, 102]
}
},
ui: {
users: {
loading: false,
error: null,
selectedUserId: 1,
filters: { search: '', role: 'all' }
},
posts: {
loading: true,
error: null,
selectedPostId: null,
filters: { status: 'all' }
}
},
auth: {
user: 1,
token: 'jwt-token',
isAuthenticated: true
}
};
// ❌ 避免的状态结构 - 深度嵌套、冗余
const badStateStructure = {
users: [
{
id: 1,
name: 'Alice',
email: 'alice@example.com',
posts: [
{ id: 101, title: 'Hello' },
{ id: 102, title: 'World' }
]
}
]
};
// 2. 不可变性最佳实践
const createImmutableReducer = () => {
return (state = initialState, action) => {
switch (action.type) {
case 'ADD_USER':
// ✅ 正确的不可变更新
return {
...state,
entities: {
...state.entities,
users: {
...state.entities.users,
byId: {
...state.entities.users.byId,
[action.user.id]: action.user
},
allIds: [...state.entities.users.allIds, action.user.id]
}
}
};
case 'UPDATE_USER':
// ✅ 嵌套对象的正确更新
return {
...state,
entities: {
...state.entities,
users: {
...state.entities.users,
byId: {
...state.entities.users.byId,
[action.userId]: {
...state.entities.users.byId[action.userId],
...action.updates
}
}
}
}
};
case 'DELETE_USER':
// ✅ 正确的删除操作
const { [action.userId]: deletedUser, ...remainingUsers } = state.entities.users.byId;
return {
...state,
entities: {
...state.entities,
users: {
byId: remainingUsers,
allIds: state.entities.users.allIds.filter(id => id !== action.userId)
}
}
};
default:
return state;
}
};
};
// 3. 使用 Immer 简化不可变操作
import { produce } from 'immer';
const createImmerReducer = () => {
return produce((draft, action) => {
switch (action.type) {
case 'ADD_USER':
draft.entities.users.byId[action.user.id] = action.user;
draft.entities.users.allIds.push(action.user.id);
break;
case 'UPDATE_USER':
Object.assign(draft.entities.users.byId[action.userId], action.updates);
break;
case 'DELETE_USER':
delete draft.entities.users.byId[action.userId];
draft.entities.users.allIds = draft.entities.users.allIds.filter(id => id !== action.userId);
break;
case 'SET_LOADING':
draft.ui.users.loading = action.loading;
break;
}
}, initialState);
};
4.4.2 性能优化策略
状态选择器优化:
import { createSelector } from 'reselect';
// 1. 基础选择器
const selectUsers = state => state.entities.users.byId;
const selectUserIds = state => state.entities.users.allIds;
const selectUsersLoading = state => state.ui.users.loading;
const selectUsersFilters = state => state.ui.users.filters;
const selectAuthUser = state => state.auth.user;
// 2. 记忆化选择器
export const selectAllUsers = createSelector(
[selectUsers, selectUserIds],
(usersById, userIds) => userIds.map(id => usersById[id])
);
export const selectActiveUsers = createSelector(
[selectAllUsers],
(users) => users.filter(user => user.status === 'active')
);
export const selectFilteredUsers = createSelector(
[selectAllUsers, selectUsersFilters],
(users, filters) => {
return users.filter(user => {
if (filters.search && !user.name.toLowerCase().includes(filters.search.toLowerCase())) {
return false;
}
if (filters.role !== 'all' && user.role !== filters.role) {
return false;
}
return true;
});
}
);
export const selectUsersWithPosts = createSelector(
[selectAllUsers, state => state.entities.posts.byId],
(users, postsById) => {
return users.map(user => ({
...user,
posts: user.postIds ? user.postIds.map(postId => postsById[postId]).filter(Boolean) : []
}));
}
);
export const selectUserStatistics = createSelector(
[selectAllUsers, selectAllUsers],
(allUsers, activeUsers) => ({
total: allUsers.length,
active: activeUsers.length,
inactive: allUsers.length - activeUsers.length,
percentage: ((activeUsers.length / allUsers.length) * 100).toFixed(1)
})
);
// 3. 动态选择器工厂
export const selectUserById = (userId) => createSelector(
[selectUsers],
(users) => users[userId]
);
export const selectUsersByRole = (role) => createSelector(
[selectAllUsers],
(users) => users.filter(user => user.role === role)
);
// 4. 组件中使用优化选择器
const UserListComponent = () => {
const dispatch = useDispatch();
// 使用优化的选择器
const filteredUsers = useSelector(selectFilteredUsers);
const statistics = useSelector(selectUserStatistics);
const loading = useSelector(selectUsersLoading);
const filters = useSelector(selectUsersFilters);
// 避免不必要的渲染
const memoizedHandleFilterChange = useCallback((filterType, value) => {
dispatch(setFilters({ [filterType]: value }));
}, [dispatch]);
if (loading) {
return <div>Loading users...</div>;
}
return (
<div className="user-list">
<UserStats statistics={statistics} />
<UserFilters filters={filters} onChange={memoizedHandleFilterChange} />
<UserTable users={filteredUsers} />
</div>
);
};
组件优化策略:
import React, { memo, useMemo, useCallback } from 'react';
import { useSelector, useDispatch } from 'react-redux';
// 1. 记忆化组件
const UserCard = memo(({ user, onSelect, onUpdate, onDelete }) => {
const memoizedOnSelect = useCallback(() => {
onSelect(user.id);
}, [onSelect, user.id]);
const memoizedOnUpdate = useCallback((updates) => {
onUpdate(user.id, updates);
}, [onUpdate, user.id]);
const memoizedOnDelete = useCallback(() => {
onDelete(user.id);
}, [onDelete, user.id]);
// 记忆化计算属性
const statusColor = useMemo(() => {
switch (user.status) {
case 'active': return '#28a745';
case 'inactive': return '#6c757d';
case 'suspended': return '#dc3545';
default: return '#6c757d';
}
}, [user.status]);
const formattedJoinDate = useMemo(() => {
return new Date(user.joinDate).toLocaleDateString();
}, [user.joinDate]);
return (
<div className="user-card">
<div className="user-header">
<img src={user.avatar} alt={user.name} className="avatar" />
<h3>{user.name}</h3>
<span
className="status-indicator"
style={{ backgroundColor: statusColor }}
>
{user.status}
</span>
</div>
<div className="user-info">
<p>{user.email}</p>
<p>Role: {user.role}</p>
<p>Joined: {formattedJoinDate}</p>
</div>
<div className="user-actions">
<button onClick={memoizedOnSelect}>View</button>
<button onClick={memoizedOnUpdate}>Edit</button>
<button onClick={memoizedOnDelete}>Delete</button>
</div>
</div>
);
});
UserCard.displayName = 'UserCard';
// 2. 虚拟化长列表
const VirtualizedUserList = memo(({ users, onUserSelect, onUserUpdate, onUserDelete }) => {
const [visibleRange, setVisibleRange] = useState({ start: 0, end: 20 });
const itemHeight = 120;
const containerRef = useRef();
const visibleUsers = useMemo(() => {
return users.slice(visibleRange.start, visibleRange.end);
}, [users, visibleRange]);
const handleScroll = useCallback((e) => {
const scrollTop = e.target.scrollTop;
const containerHeight = e.target.clientHeight;
const start = Math.floor(scrollTop / itemHeight);
const visibleCount = Math.ceil(containerHeight / itemHeight);
const end = start + visibleCount + 5; // 缓冲区
setVisibleRange({ start, end });
}, []);
return (
<div
ref={containerRef}
className="virtualized-list"
style={{ height: '600px', overflow: 'auto' }}
onScroll={handleScroll}
>
<div style={{ height: users.length * itemHeight, position: 'relative' }}>
{visibleUsers.map((user, index) => (
<div
key={user.id}
style={{
position: 'absolute',
top: (visibleRange.start + index) * itemHeight,
height: itemHeight,
width: '100%'
}}
>
<UserCard
user={user}
onSelect={onUserSelect}
onUpdate={onUserUpdate}
onDelete={onUserDelete}
/>
</div>
))}
</div>
</div>
);
});
// 3. 组件分割与懒加载
const UserManagement = () => {
const [activeTab, setActiveTab] = useState('list');
const UserListTab = useMemo(() => React.lazy(() => import('./UserListTab')), []);
const UserStatsTab = useMemo(() => React.lazy(() => import('./UserStatsTab')), []);
const UserSettingsTab = useMemo(() => React.lazy(() => import('./UserSettingsTab')), []);
return (
<div className="user-management">
<div className="tabs">
<button
onClick={() => setActiveTab('list')}
className={activeTab === 'list' ? 'active' : ''}
>
Users List
</button>
<button
onClick={() => setActiveTab('stats')}
className={activeTab === 'stats' ? 'active' : ''}
>
Statistics
</button>
<button
onClick={() => setActiveTab('settings')}
className={activeTab === 'settings' ? 'active' : ''}
>
Settings
</button>
</div>
<React.Suspense fallback={<div>Loading...</div>}>
{activeTab === 'list' && <UserListTab />}
{activeTab === 'stats' && <UserStatsTab />}
{activeTab === 'settings' && <UserSettingsTab />}
</React.Suspense>
</div>
);
};
4.4.3 错误处理与调试
全局错误边界与状态恢复:
import React, { Component } from 'react';
import { useDispatch } from 'react-redux';
import { resetError } from './slices/errorSlice';
// 1. 全局错误边界
class GlobalErrorBoundary extends Component {
constructor(props) {
super(props);
this.state = {
hasError: false,
error: null,
errorInfo: null,
errorCount: 0
};
}
static getDerivedStateFromError(error) {
return { hasError: true };
}
componentDidCatch(error, errorInfo) {
console.error('Global error caught:', error, errorInfo);
this.setState({
error,
errorInfo,
errorCount: this.state.errorCount + 1
});
// 发送错误报告
this.reportError(error, errorInfo);
// 尝试恢复状态
this.attemptRecovery();
}
reportError = async (error, errorInfo) => {
try {
await fetch('/api/errors', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
error: error.toString(),
stack: error.stack,
componentStack: errorInfo.componentStack,
timestamp: new Date().toISOString(),
userAgent: navigator.userAgent,
url: window.location.href
})
});
} catch (reportError) {
console.error('Failed to report error:', reportError);
}
};
attemptRecovery = () => {
// 尝试恢复应用的稳定状态
const { dispatch } = this.props;
// 重置各种错误状态
dispatch(resetError());
// 清理可能损坏的状态
localStorage.clear();
// 如果错误次数过多,建议用户刷新页面
if (this.state.errorCount > 3) {
this.setState({ suggestRefresh: true });
}
};
handleRetry = () => {
this.setState({ hasError: false, error: null, errorInfo: null });
};
handleRefresh = () => {
window.location.reload();
};
render() {
if (this.state.hasError) {
if (this.state.suggestRefresh) {
return (
<div className="error-boundary critical">
<h2>Something went wrong</h2>
<p>The application has encountered multiple errors.</p>
<p>Please refresh the page to continue.</p>
<button onClick={this.handleRefresh}>
Refresh Page
</button>
<details style={{ marginTop: '20px' }}>
<summary>Error Details</summary>
<pre>{this.state.error && this.state.error.toString()}</pre>
<pre>{this.state.errorInfo.componentStack}</pre>
</details>
</div>
);
}
return (
<div className="error-boundary">
<h2>Something went wrong</h2>
<p>We're sorry, but something unexpected happened.</p>
<div className="error-actions">
<button onClick={this.handleRetry}>
Try Again
</button>
<button onClick={this.handleRefresh}>
Refresh Page
</button>
</div>
<details style={{ marginTop: '20px' }}>
<summary>Technical Details</summary>
<pre>{this.state.error && this.state.error.toString()}</pre>
<pre>{this.state.errorInfo.componentStack}</pre>
</details>
</div>
);
}
return this.props.children;
}
}
// 2. 错误状态管理 Slice
const errorSlice = createSlice({
name: 'error',
initialState: {
errors: [],
globalError: null,
recoveryAttempts: 0
},
reducers: {
addError: (state, action) => {
const error = {
id: Date.now(),
timestamp: new Date().toISOString(),
...action.payload
};
state.errors.push(error);
// 保持最近50个错误
if (state.errors.length > 50) {
state.errors = state.errors.slice(-50);
}
},
removeError: (state, action) => {
state.errors = state.errors.filter(error => error.id !== action.payload);
},
setGlobalError: (state, action) => {
state.globalError = {
...action.payload,
timestamp: new Date().toISOString()
};
},
clearGlobalError: (state) => {
state.globalError = null;
},
resetError: (state) => {
state.errors = [];
state.globalError = null;
},
incrementRecoveryAttempts: (state) => {
state.recoveryAttempts += 1;
},
resetRecoveryAttempts: (state) => {
state.recoveryAttempts = 0;
}
}
});
export const {
addError,
removeError,
setGlobalError,
clearGlobalError,
resetError,
incrementRecoveryAttempts,
resetRecoveryAttempts
} = errorSlice.actions;
export default errorSlice.reducer;
// 3. 错误处理中间件
const errorHandlerMiddleware = (store) => (next) => (action) => {
try {
return next(action);
} catch (error) {
console.error('Redux action error:', error, action);
store.dispatch(addError({
type: 'REDUX_ACTION_ERROR',
action: action.type,
error: error.toString(),
stack: error.stack
}));
// 可以根据错误类型进行不同的处理
if (error.type === 'NETWORK_ERROR') {
store.dispatch(setGlobalError({
message: 'Network connection failed. Please check your internet connection.',
type: 'error'
}));
}
return action;
}
};
// 4. 调试工具配置
const configureStoreWithDevTools = () => {
const store = configureStore({
reducer: {
// ... 其他 reducers
error: errorSlice
},
middleware: (getDefaultMiddleware) =>
getDefaultMiddleware({
serializableCheck: {
// 忽略非序列化值的检查(用于调试)
ignoredActions: ['persist/PERSIST', 'persist/REHYDRATE'],
ignoredPaths: ['error.errors'] // 错误对象可能包含非序列化数据
}
}).concat(errorHandlerMiddleware),
devTools: {
name: 'My React App',
trace: true,
traceLimit: 25
}
});
// 热重载支持
if (process.env.NODE_ENV === 'development') {
if (module.hot) {
module.hot.accept('./rootReducer', () => {
const newRootReducer = require('./rootReducer').default;
store.replaceReducer(newRootReducer);
});
}
}
return store;
};
// 5. 状态恢复工具
const StateRecovery = () => {
const dispatch = useDispatch();
const { errors, globalError, recoveryAttempts } = useSelector(state => state.error);
const attemptStateRecovery = useCallback(() => {
dispatch(incrementRecoveryAttempts());
// 清理可能损坏的状态
dispatch(resetError());
// 重新初始化关键状态
dispatch({ type: 'RESET_APPLICATION_STATE' });
// 记录恢复尝试
console.log('State recovery attempt:', recoveryAttempts + 1);
}, [dispatch, recoveryAttempts]);
const clearErrors = useCallback(() => {
dispatch(resetError());
dispatch(resetRecoveryAttempts());
}, [dispatch]);
if (errors.length === 0 && !globalError) {
return null;
}
return (
<div className="state-recovery">
{globalError && (
<div className="global-error">
<h3>Global Error</h3>
<p>{globalError.message}</p>
<button onClick={attemptStateRecovery}>
Attempt Recovery
</button>
<button onClick={() => dispatch(clearGlobalError())}>
Dismiss
</button>
</div>
)}
{errors.length > 0 && (
<div className="error-log">
<h4>Recent Errors ({errors.length})</h4>
<ul>
{errors.slice(-5).map(error => (
<li key={error.id}>
<small>{error.timestamp}</small>
<p>{error.message}</p>
<button onClick={() => dispatch(removeError(error.id))}>
Clear
</button>
</li>
))}
</ul>
<div className="recovery-actions">
<button onClick={attemptStateRecovery}>
Recovery (Attempt {recoveryAttempts})
</button>
<button onClick={clearErrors}>
Clear All Errors
</button>
</div>
</div>
)}
</div>
);
};
4.4.4 测试策略
状态管理测试:
import { configureStore } from '@reduxjs/toolkit';
import { render, screen, fireEvent, waitFor } from '@testing-library/react';
import { Provider } from 'react-redux';
import userReducer, {
fetchUsers,
createUser,
updateUserLocal,
selectFilteredUsers
} from './slices/usersSlice';
// 1. Reducer 测试
describe('usersSlice', () => {
const initialState = {
users: [],
loading: false,
error: null,
filters: { search: '', role: 'all', status: 'all' }
};
it('should return the initial state', () => {
expect(userReducer(undefined, { type: 'unknown' })).toEqual(initialState);
});
it('should handle setCurrentUser', () => {
const user = { id: 1, name: 'Test User' };
const action = { type: 'users/setCurrentUser', payload: user };
const state = userReducer(initialState, action);
expect(state.currentUser).toEqual(user);
});
it('should handle updateUserLocal', () => {
const stateWithUsers = {
...initialState,
users: [{ id: 1, name: 'John', email: 'john@example.com' }]
};
const updates = { name: 'John Doe' };
const action = { type: 'users/updateUserLocal', payload: { userId: 1, updates } };
const state = userReducer(stateWithUsers, action);
expect(state.users[0].name).toBe('John Doe');
expect(state.users[0].email).toBe('john@example.com');
});
});
// 2. Async Thunk 测试
describe('users async thunks', () => {
let store;
beforeEach(() => {
store = configureStore({
reducer: {
users: userReducer
}
});
});
it('should handle fetchUsers.fulfilled', async () => {
const mockUsers = [
{ id: 1, name: 'Alice' },
{ id: 2, name: 'Bob' }
];
global.fetch = jest.fn(() =>
Promise.resolve({
ok: true,
json: () => Promise.resolve(mockUsers)
})
);
await store.dispatch(fetchUsers());
const state = store.getState().users;
expect(state.loading).toBe(false);
expect(state.users).toEqual(mockUsers);
expect(state.error).toBe(null);
});
it('should handle fetchUsers.rejected', async () => {
global.fetch = jest.fn(() =>
Promise.reject(new Error('Network error'))
);
await store.dispatch(fetchUsers());
const state = store.getState().users;
expect(state.loading).toBe(false);
expect(state.users).toEqual([]);
expect(state.error).toBe('Network error');
});
it('should handle createUser.fulfilled', async () => {
const newUser = { name: 'New User', email: 'new@example.com' };
const createdUser = { id: 3, ...newUser };
global.fetch = jest.fn(() =>
Promise.resolve({
ok: true,
json: () => Promise.resolve(createdUser)
})
);
await store.dispatch(createUser(newUser));
const state = store.getState().users;
expect(state.loading).toBe(false);
expect(state.users).toContainEqual(createdUser);
expect(state.currentUser).toEqual(createdUser);
});
});
// 3. Selector 测试
describe('user selectors', () => {
const mockState = {
users: {
users: [
{ id: 1, name: 'Alice', role: 'admin', status: 'active' },
{ id: 2, name: 'Bob', role: 'user', status: 'inactive' },
{ id: 3, name: 'Charlie', role: 'user', status: 'active' }
],
filters: { search: '', role: 'all', status: 'all' }
}
};
it('selectFilteredUsers should return all users when no filters applied', () => {
const result = selectFilteredUsers(mockState);
expect(result).toHaveLength(3);
});
it('selectFilteredUsers should filter by search term', () => {
const stateWithSearchFilter = {
...mockState,
users: {
...mockState.users,
filters: { ...mockState.users.filters, search: 'Alice' }
}
};
const result = selectFilteredUsers(stateWithSearchFilter);
expect(result).toHaveLength(1);
expect(result[0].name).toBe('Alice');
});
it('selectFilteredUsers should filter by role', () => {
const stateWithRoleFilter = {
...mockState,
users: {
...mockState.users,
filters: { ...mockState.users.filters, role: 'admin' }
}
};
const result = selectFilteredUsers(stateWithRoleFilter);
expect(result).toHaveLength(1);
expect(result[0].role).toBe('admin');
});
});
// 4. 组件集成测试
const TestComponent = ({ userId }) => {
const { users, loading, error, fetchUsers } = useUsers();
useEffect(() => {
fetchUsers();
}, [fetchUsers]);
if (loading) return <div>Loading...</div>;
if (error) return <div>Error: {error}</div>;
return (
<div>
{users.map(user => (
<div key={user.id} data-testid={`user-${user.id}`}>
{user.name}
</div>
))}
</div>
);
};
const renderWithProvider = (component, initialState = {}) => {
const store = configureStore({
reducer: {
users: userReducer
},
preloadedState: initialState
});
return render(
<Provider store={store}>
{component}
</Provider>
);
};
describe('UserManagement component', () => {
beforeEach(() => {
global.fetch = jest.fn();
});
it('should render loading state initially', () => {
renderWithProvider(<TestComponent />);
expect(screen.getByText('Loading...')).toBeInTheDocument();
});
it('should render users after successful fetch', async () => {
const mockUsers = [
{ id: 1, name: 'Alice' },
{ id: 2, name: 'Bob' }
];
global.fetch.mockResolvedValueOnce({
ok: true,
json: () => Promise.resolve(mockUsers)
});
renderWithProvider(<TestComponent />);
await waitFor(() => {
expect(screen.getByTestId('user-1')).toBeInTheDocument();
expect(screen.getByTestId('user-2')).toBeInTheDocument();
});
});
it('should render error message on fetch failure', async () => {
global.fetch.mockRejectedValueOnce(new Error('Network error'));
renderWithProvider(<TestComponent />);
await waitFor(() => {
expect(screen.getByText('Error: Network error')).toBeInTheDocument();
});
});
});
// 5. 端到端状态流测试
describe('User Management E2E Flow', () => {
let store;
beforeEach(() => {
store = configureStore({
reducer: {
users: userReducer
}
});
global.fetch = jest.fn();
});
it('should complete full user management workflow', async () => {
// 1. 初始状态
let state = store.getState().users;
expect(state.users).toHaveLength(0);
expect(state.loading).toBe(false);
// 2. 创建用户
const newUser = { name: 'Test User', email: 'test@example.com' };
const createdUser = { id: 1, ...newUser };
global.fetch.mockResolvedValueOnce({
ok: true,
json: () => Promise.resolve(createdUser)
});
const createResult = await store.dispatch(createUser(newUser));
expect(createResult.meta.requestStatus).toBe('fulfilled');
// 3. 验证用户创建
state = store.getState().users;
expect(state.users).toHaveLength(1);
expect(state.users[0]).toEqual(createdUser);
// 4. 更新用户
const updates = { name: 'Updated User' };
store.dispatch(updateUserLocal({ userId: 1, updates }));
// 5. 验证用户更新
state = store.getState().users;
expect(state.users[0].name).toBe('Updated User');
expect(state.users[0].email).toBe('test@example.com'); // 其他字段保持不变
// 6. 测试过滤
store.dispatch(setFilters({ search: 'Updated' }));
state = store.getState().users;
// 使用 selector 验证过滤结果
const filteredUsers = selectFilteredUsers(state);
expect(filteredUsers).toHaveLength(1);
expect(filteredUsers[0].name).toBe('Updated User');
});
});
通过这些最佳实践和模式,你可以构建出健壮、可测试、高性能的 React 状态管理系统。关键是要根据应用的具体需求选择合适的状态管理方案,并始终关注可维护性和开发体验。

浙公网安备 33010602011771号