React状态管理完全指南
状态管理是React应用开发的核心挑战之一。随着应用规模的增长,如何高效、可维护地管理应用状态变得至关重要。本指南将深入探讨各种状态管理方案,帮助你选择最适合项目需求的解决方案。
核心价值
状态管理 = 数据流控制 + 状态共享 + 性能优化 + 开发体验
- 🎯 数据流控制:单向数据流,可预测的状态变化
- 🔄 状态共享:跨组件状态共享,避免prop drilling
- ⚡ 性能优化:精确更新,避免不必要的重新渲染
- 🛠️ 开发体验:调试工具,时间旅行,状态持久化
- 📊 可维护性:清晰的状态结构,易于测试和维护
- 🎨 灵活性:适应不同规模和复杂度的应用需求
1. 状态管理方案对比
1.1 方案选择指南
选择合适的状态管理方案需要考虑应用规模、团队经验、性能要求等多个因素。
状态管理方案对比表
| 方案 | 学习曲线 | 包大小 | 性能 | 生态系统 | 适用场景 | TypeScript支持 |
|---|---|---|---|---|---|---|
| useState + useContext | 低 | 0KB | 中等 | React内置 | 小型应用 | ⭐⭐⭐ |
| Redux Toolkit | 中等 | 47KB | 高 | 丰富 | 中大型应用 | ⭐⭐⭐⭐⭐ |
| Zustand | 低 | 8KB | 高 | 中等 | 中小型应用 | ⭐⭐⭐⭐ |
| Jotai | 中等 | 13KB | 高 | 新兴 | 原子化状态 | ⭐⭐⭐⭐⭐ |
| Recoil | 中等 | 79KB | 高 | 复杂状态图 | ⭐⭐⭐⭐ | |
| MobX | 高 | 16KB | 高 | 成熟 | 面向对象 | ⭐⭐⭐⭐ |
| Valtio | 低 | 9KB | 高 | 新兴 | 代理状态 | ⭐⭐⭐⭐ |
- React内置状态
- Redux Toolkit
- Zustand
- Jotai原子化
React内置状态管理
对于小到中型应用,React的内置状态管理通常已经足够。
React内置状态管理最佳实践
typescript
1import React, { createContext, useContext, useReducer, useState, useCallback, useMemo } from 'react';23// 1. 简单状态管理 - useState + useContext4interface AppState {5 user: User | null;6 theme: 'light' | 'dark';7 language: 'zh' | 'en';8 notifications: Notification[];9}1011interface User {12 id: number;13 name: string;14 email: string;15 avatar?: string;16}1718interface Notification {19 id: string;20 type: 'success' | 'error' | 'warning' | 'info';21 message: string;22 timestamp: number;23}2425// Context定义26const AppStateContext = createContext<{27 state: AppState;28 actions: {29 setUser: (user: User | null) => void;30 toggleTheme: () => void;31 setLanguage: (language: 'zh' | 'en') => void;32 addNotification: (notification: Omit<Notification, 'id' | 'timestamp'>) => void;33 removeNotification: (id: string) => void;34 clearNotifications: () => void;35 };36} | undefined>(undefined);3738// Provider组件39export const AppStateProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {40 const [state, setState] = useState<AppState>({41 user: null,42 theme: 'light',43 language: 'zh',44 notifications: []45 });46 47 // 优化的action creators48 const actions = useMemo(() => ({49 setUser: (user: User | null) => {50 setState(prev => ({ ...prev, user }));51 },52 53 toggleTheme: () => {54 setState(prev => ({ 55 ...prev, 56 theme: prev.theme === 'light' ? 'dark' : 'light' 57 }));58 },59 60 setLanguage: (language: 'zh' | 'en') => {61 setState(prev => ({ ...prev, language }));62 },63 64 addNotification: (notification: Omit<Notification, 'id' | 'timestamp'>) => {65 const newNotification: Notification = {66 ...notification,67 id: Math.random().toString(36).substr(2, 9),68 timestamp: Date.now()69 };70 71 setState(prev => ({72 ...prev,73 notifications: [...prev.notifications, newNotification]74 }));75 76 // 自动移除通知77 setTimeout(() => {78 setState(prev => ({79 ...prev,80 notifications: prev.notifications.filter(n => n.id !== newNotification.id)81 }));82 }, 5000);83 },84 85 removeNotification: (id: string) => {86 setState(prev => ({87 ...prev,88 notifications: prev.notifications.filter(n => n.id !== id)89 }));90 },91 92 clearNotifications: () => {93 setState(prev => ({ ...prev, notifications: [] }));94 }95 }), []);96 97 const value = useMemo(() => ({ state, actions }), [state, actions]);98 99 return (100 <AppStateContext.Provider value={value}>101 {children}102 </AppStateContext.Provider>103 );104};105106// Hook107export const useAppState = () => {108 const context = useContext(AppStateContext);109 if (context === undefined) {110 throw new Error('useAppState must be used within an AppStateProvider');111 }112 return context;113};114115// 2. 复杂状态管理 - useReducer116interface TodoState {117 todos: Todo[];118 filter: 'all' | 'active' | 'completed';119 loading: boolean;120 error: string | null;121}122123interface Todo {124 id: string;125 text: string;126 completed: boolean;127 createdAt: number;128 updatedAt: number;129}130131type TodoAction =132 | { type: 'ADD_TODO'; payload: { text: string } }133 | { type: 'TOGGLE_TODO'; payload: { id: string } }134 | { type: 'DELETE_TODO'; payload: { id: string } }135 | { type: 'EDIT_TODO'; payload: { id: string; text: string } }136 | { type: 'SET_FILTER'; payload: { filter: TodoState['filter'] } }137 | { type: 'SET_LOADING'; payload: { loading: boolean } }138 | { type: 'SET_ERROR'; payload: { error: string | null } }139 | { type: 'LOAD_TODOS'; payload: { todos: Todo[] } }140 | { type: 'CLEAR_COMPLETED' };141142const todoReducer = (state: TodoState, action: TodoAction): TodoState => {143 switch (action.type) {144 case 'ADD_TODO': {145 const newTodo: Todo = {146 id: Math.random().toString(36).substr(2, 9),147 text: action.payload.text,148 completed: false,149 createdAt: Date.now(),150 updatedAt: Date.now()151 };152 153 return {154 ...state,155 todos: [...state.todos, newTodo],156 error: null157 };158 }159 160 case 'TOGGLE_TODO':161 return {162 ...state,163 todos: state.todos.map(todo =>164 todo.id === action.payload.id165 ? { ...todo, completed: !todo.completed, updatedAt: Date.now() }166 : todo167 )168 };169 170 case 'DELETE_TODO':171 return {172 ...state,173 todos: state.todos.filter(todo => todo.id !== action.payload.id)174 };175 176 case 'EDIT_TODO':177 return {178 ...state,179 todos: state.todos.map(todo =>180 todo.id === action.payload.id181 ? { ...todo, text: action.payload.text, updatedAt: Date.now() }182 : todo183 )184 };185 186 case 'SET_FILTER':187 return {188 ...state,189 filter: action.payload.filter190 };191 192 case 'SET_LOADING':193 return {194 ...state,195 loading: action.payload.loading196 };197 198 case 'SET_ERROR':199 return {200 ...state,201 error: action.payload.error,202 loading: false203 };204 205 case 'LOAD_TODOS':206 return {207 ...state,208 todos: action.payload.todos,209 loading: false,210 error: null211 };212 213 case 'CLEAR_COMPLETED':214 return {215 ...state,216 todos: state.todos.filter(todo => !todo.completed)217 };218 219 default:220 return state;221 }222};223224// Todo Context225const TodoContext = createContext<{226 state: TodoState;227 dispatch: React.Dispatch<TodoAction>;228 actions: {229 addTodo: (text: string) => void;230 toggleTodo: (id: string) => void;231 deleteTodo: (id: string) => void;232 editTodo: (id: string, text: string) => void;233 setFilter: (filter: TodoState['filter']) => void;234 clearCompleted: () => void;235 loadTodos: () => Promise<void>;236 };237} | undefined>(undefined);238239export const TodoProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {240 const [state, dispatch] = useReducer(todoReducer, {241 todos: [],242 filter: 'all',243 loading: false,244 error: null245 });246 247 // Action creators248 const actions = useMemo(() => ({249 addTodo: (text: string) => {250 dispatch({ type: 'ADD_TODO', payload: { text } });251 },252 253 toggleTodo: (id: string) => {254 dispatch({ type: 'TOGGLE_TODO', payload: { id } });255 },256 257 deleteTodo: (id: string) => {258 dispatch({ type: 'DELETE_TODO', payload: { id } });259 },260 261 editTodo: (id: string, text: string) => {262 dispatch({ type: 'EDIT_TODO', payload: { id, text } });263 },264 265 setFilter: (filter: TodoState['filter']) => {266 dispatch({ type: 'SET_FILTER', payload: { filter } });267 },268 269 clearCompleted: () => {270 dispatch({ type: 'CLEAR_COMPLETED' });271 },272 273 loadTodos: async () => {274 dispatch({ type: 'SET_LOADING', payload: { loading: true } });275 276 try {277 // 模拟API调用278 const response = await fetch('/api/todos');279 if (!response.ok) {280 throw new Error('Failed to load todos');281 }282 283 const todos = await response.json();284 dispatch({ type: 'LOAD_TODOS', payload: { todos } });285 } catch (error) {286 dispatch({ 287 type: 'SET_ERROR', 288 payload: { error: error instanceof Error ? error.message : 'Unknown error' }289 });290 }291 }292 }), []);293 294 const value = useMemo(() => ({ state, dispatch, actions }), [state, actions]);295 296 return (297 <TodoContext.Provider value={value}>298 {children}299 </TodoContext.Provider>300 );301};302303export const useTodos = () => {304 const context = useContext(TodoContext);305 if (context === undefined) {306 throw new Error('useTodos must be used within a TodoProvider');307 }308 return context;309};310311// 使用示例312const TodoApp: React.FC = () => {313 const { state, actions } = useTodos();314 const [newTodoText, setNewTodoText] = useState('');315 316 // 过滤todos317 const filteredTodos = useMemo(() => {318 switch (state.filter) {319 case 'active':320 return state.todos.filter(todo => !todo.completed);321 case 'completed':322 return state.todos.filter(todo => todo.completed);323 default:324 return state.todos;325 }326 }, [state.todos, state.filter]);327 328 const handleAddTodo = useCallback((e: React.FormEvent) => {329 e.preventDefault();330 if (newTodoText.trim()) {331 actions.addTodo(newTodoText.trim());332 setNewTodoText('');333 }334 }, [newTodoText, actions]);335 336 useEffect(() => {337 actions.loadTodos();338 }, [actions]);339 340 if (state.loading) {341 return <div className="loading">加载中...</div>;342 }343 344 if (state.error) {345 return <div className="error">错误: {state.error}</div>;346 }347 348 return (349 <div className="todo-app">350 <h1>待办事项</h1>351 352 <form onSubmit={handleAddTodo} className="add-todo-form">353 <input354 type="text"355 value={newTodoText}356 onChange={(e) => setNewTodoText(e.target.value)}357 placeholder="添加新的待办事项..."358 className="todo-input"359 />360 <button type="submit">添加</button>361 </form>362 363 <div className="todo-filters">364 <button365 className={state.filter === 'all' ? 'active' : ''}366 onClick={() => actions.setFilter('all')}367 >368 全部369 </button>370 <button371 className={state.filter === 'active' ? 'active' : ''}372 onClick={() => actions.setFilter('active')}373 >374 未完成375 </button>376 <button377 className={state.filter === 'completed' ? 'active' : ''}378 onClick={() => actions.setFilter('completed')}379 >380 已完成381 </button>382 </div>383 384 <ul className="todo-list">385 {filteredTodos.map(todo => (386 <TodoItem387 key={todo.id}388 todo={todo}389 onToggle={() => actions.toggleTodo(todo.id)}390 onDelete={() => actions.deleteTodo(todo.id)}391 onEdit={(text) => actions.editTodo(todo.id, text)}392 />393 ))}394 </ul>395 396 {state.todos.some(todo => todo.completed) && (397 <button onClick={actions.clearCompleted} className="clear-completed">398 清除已完成399 </button>400 )}401 </div>402 );403};Redux Toolkit现代化状态管理
Redux Toolkit是Redux官方推荐的现代化工具集,大幅简化了Redux的使用。
Redux Toolkit完整实现
typescript
1import { createSlice, createAsyncThunk, configureStore, PayloadAction } from '@reduxjs/toolkit';2import { useSelector, useDispatch, TypedUseSelectorHook } from 'react-redux';34// 1. 用户状态管理5interface User {6 id: number;7 name: string;8 email: string;9 avatar?: string;10 role: 'admin' | 'user' | 'guest';11}1213interface UserState {14 currentUser: User | null;15 users: User[];16 loading: boolean;17 error: string | null;18}1920const initialUserState: UserState = {21 currentUser: null,22 users: [],23 loading: false,24 error: null25};2627// 异步Thunk28export const fetchUsers = createAsyncThunk(29 'users/fetchUsers',30 async (_, { rejectWithValue }) => {31 try {32 const response = await fetch('/api/users');33 if (!response.ok) {34 throw new Error('Failed to fetch users');35 }36 return await response.json();37 } catch (error) {38 return rejectWithValue(error instanceof Error ? error.message : 'Unknown error');39 }40 }41);4243export const loginUser = createAsyncThunk(44 'users/loginUser',45 async ({ email, password }: { email: string; password: string }, { rejectWithValue }) => {46 try {47 const response = await fetch('/api/auth/login', {48 method: 'POST',49 headers: { 'Content-Type': 'application/json' },50 body: JSON.stringify({ email, password })51 });52 53 if (!response.ok) {54 throw new Error('Login failed');55 }56 57 const data = await response.json();58 localStorage.setItem('token', data.token);59 return data.user;60 } catch (error) {61 return rejectWithValue(error instanceof Error ? error.message : 'Login failed');62 }63 }64);6566export const updateUser = createAsyncThunk(67 'users/updateUser',68 async ({ id, updates }: { id: number; updates: Partial<User> }, { rejectWithValue }) => {69 try {70 const token = localStorage.getItem('token');71 const response = await fetch(`/api/users/${id}`, {72 method: 'PATCH',73 headers: {74 'Content-Type': 'application/json',75 'Authorization': `Bearer ${token}`76 },77 body: JSON.stringify(updates)78 });79 80 if (!response.ok) {81 throw new Error('Update failed');82 }83 84 return await response.json();85 } catch (error) {86 return rejectWithValue(error instanceof Error ? error.message : 'Update failed');87 }88 }89);9091// User Slice92const userSlice = createSlice({93 name: 'users',94 initialState: initialUserState,95 reducers: {96 logout: (state) => {97 state.currentUser = null;98 localStorage.removeItem('token');99 },100 clearError: (state) => {101 state.error = null;102 },103 setCurrentUser: (state, action: PayloadAction<User>) => {104 state.currentUser = action.payload;105 }106 },107 extraReducers: (builder) => {108 builder109 // fetchUsers110 .addCase(fetchUsers.pending, (state) => {111 state.loading = true;112 state.error = null;113 })114 .addCase(fetchUsers.fulfilled, (state, action) => {115 state.loading = false;116 state.users = action.payload;117 })118 .addCase(fetchUsers.rejected, (state, action) => {119 state.loading = false;120 state.error = action.payload as string;121 })122 // loginUser123 .addCase(loginUser.pending, (state) => {124 state.loading = true;125 state.error = null;126 })127 .addCase(loginUser.fulfilled, (state, action) => {128 state.loading = false;129 state.currentUser = action.payload;130 })131 .addCase(loginUser.rejected, (state, action) => {132 state.loading = false;133 state.error = action.payload as string;134 })135 // updateUser136 .addCase(updateUser.fulfilled, (state, action) => {137 if (state.currentUser && state.currentUser.id === action.payload.id) {138 state.currentUser = action.payload;139 }140 state.users = state.users.map(user =>141 user.id === action.payload.id ? action.payload : user142 );143 });144 }145});146147export const { logout, clearError, setCurrentUser } = userSlice.actions;148149// 2. 购物车状态管理150interface CartItem {151 id: number;152 name: string;153 price: number;154 quantity: number;155 image: string;156}157158interface CartState {159 items: CartItem[];160 totalItems: number;161 totalPrice: number;162 discount: number;163 shippingCost: number;164}165166const initialCartState: CartState = {167 items: [],168 totalItems: 0,169 totalPrice: 0,170 discount: 0,171 shippingCost: 0172};173174const cartSlice = createSlice({175 name: 'cart',176 initialState: initialCartState,177 reducers: {178 addItem: (state, action: PayloadAction<Omit<CartItem, 'quantity'>>) => {179 const existingItem = state.items.find(item => item.id === action.payload.id);180 181 if (existingItem) {182 existingItem.quantity += 1;183 } else {184 state.items.push({ ...action.payload, quantity: 1 });185 }186 187 cartSlice.caseReducers.calculateTotals(state);188 },189 190 removeItem: (state, action: PayloadAction<number>) => {191 state.items = state.items.filter(item => item.id !== action.payload);192 cartSlice.caseReducers.calculateTotals(state);193 },194 195 updateQuantity: (state, action: PayloadAction<{ id: number; quantity: number }>) => {196 const { id, quantity } = action.payload;197 198 if (quantity <= 0) {199 state.items = state.items.filter(item => item.id !== id);200 } else {201 const item = state.items.find(item => item.id === id);202 if (item) {203 item.quantity = quantity;204 }205 }206 207 cartSlice.caseReducers.calculateTotals(state);208 },209 210 applyDiscount: (state, action: PayloadAction<number>) => {211 state.discount = action.payload;212 },213 214 setShippingCost: (state, action: PayloadAction<number>) => {215 state.shippingCost = action.payload;216 },217 218 clearCart: (state) => {219 state.items = [];220 state.totalItems = 0;221 state.totalPrice = 0;222 state.discount = 0;223 },224 225 calculateTotals: (state) => {226 state.totalItems = state.items.reduce((sum, item) => sum + item.quantity, 0);227 state.totalPrice = state.items.reduce((sum, item) => sum + item.price * item.quantity, 0);228 }229 }230});231232export const {233 addItem,234 removeItem,235 updateQuantity,236 applyDiscount,237 setShippingCost,238 clearCart239} = cartSlice.actions;240241// 3. 通知状态管理242interface Notification {243 id: string;244 type: 'success' | 'error' | 'warning' | 'info';245 message: string;246 timestamp: number;247 autoClose?: boolean;248}249250interface NotificationState {251 notifications: Notification[];252}253254const initialNotificationState: NotificationState = {255 notifications: []256};257258const notificationSlice = createSlice({259 name: 'notifications',260 initialState: initialNotificationState,261 reducers: {262 addNotification: (state, action: PayloadAction<Omit<Notification, 'id' | 'timestamp'>>) => {263 const notification: Notification = {264 ...action.payload,265 id: Math.random().toString(36).substr(2, 9),266 timestamp: Date.now()267 };268 269 state.notifications.push(notification);270 },271 272 removeNotification: (state, action: PayloadAction<string>) => {273 state.notifications = state.notifications.filter(n => n.id !== action.payload);274 },275 276 clearNotifications: (state) => {277 state.notifications = [];278 }279 }280});281282export const { addNotification, removeNotification, clearNotifications } = notificationSlice.actions;283284// Store配置285export const store = configureStore({286 reducer: {287 users: userSlice.reducer,288 cart: cartSlice.reducer,289 notifications: notificationSlice.reducer290 },291 middleware: (getDefaultMiddleware) =>292 getDefaultMiddleware({293 serializableCheck: {294 ignoredActions: ['persist/PERSIST', 'persist/REHYDRATE']295 }296 }),297 devTools: process.env.NODE_ENV !== 'production'298});299300export type RootState = ReturnType<typeof store.getState>;301export type AppDispatch = typeof store.dispatch;302303// 类型化的hooks304export const useAppDispatch = () => useDispatch<AppDispatch>();305export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector;306307// Selectors308export const selectCurrentUser = (state: RootState) => state.users.currentUser;309export const selectUsers = (state: RootState) => state.users.users;310export const selectUserLoading = (state: RootState) => state.users.loading;311export const selectUserError = (state: RootState) => state.users.error;312313export const selectCartItems = (state: RootState) => state.cart.items;314export const selectCartTotal = (state: RootState) => state.cart.totalPrice;315export const selectCartItemCount = (state: RootState) => state.cart.totalItems;316export const selectCartFinalPrice = (state: RootState) => {317 const { totalPrice, discount, shippingCost } = state.cart;318 const discountAmount = totalPrice * (discount / 100);319 return totalPrice - discountAmount + shippingCost;320};321322export const selectNotifications = (state: RootState) => state.notifications.notifications;323324// 使用示例组件325const UserProfile: React.FC = () => {326 const dispatch = useAppDispatch();327 const currentUser = useAppSelector(selectCurrentUser);328 const loading = useAppSelector(selectUserLoading);329 const error = useAppSelector(selectUserError);330 331 const [editing, setEditing] = useState(false);332 const [name, setName] = useState(currentUser?.name || '');333 334 const handleSave = async () => {335 if (currentUser) {336 try {337 await dispatch(updateUser({ id: currentUser.id, updates: { name } })).unwrap();338 setEditing(false);339 dispatch(addNotification({340 type: 'success',341 message: '用户信息更新成功',342 autoClose: true343 }));344 } catch (error) {345 dispatch(addNotification({346 type: 'error',347 message: '更新失败: ' + error,348 autoClose: true349 }));350 }351 }352 };353 354 if (loading) return <div>加载中...</div>;355 if (error) return <div>错误: {error}</div>;356 if (!currentUser) return <div>请先登录</div>;357 358 return (359 <div className="user-profile">360 <h2>用户资料</h2>361 362 {editing ? (363 <div>364 <input365 type="text"366 value={name}367 onChange={(e) => setName(e.target.value)}368 />369 <button onClick={handleSave}>保存</button>370 <button onClick={() => setEditing(false)}>取消</button>371 </div>372 ) : (373 <div>374 <p>姓名: {currentUser.name}</p>375 <p>邮箱: {currentUser.email}</p>376 <button onClick={() => setEditing(true)}>编辑</button>377 </div>378 )}379 380 <button onClick={() => dispatch(logout())}>381 退出登录382 </button>383 </div>384 );385};386387const ShoppingCart: React.FC = () => {388 const dispatch = useAppDispatch();389 const items = useAppSelector(selectCartItems);390 const totalPrice = useAppSelector(selectCartTotal);391 const finalPrice = useAppSelector(selectCartFinalPrice);392 393 return (394 <div className="shopping-cart">395 <h2>购物车</h2>396 397 {items.length === 0 ? (398 <p>购物车为空</p>399 ) : (400 <>401 {items.map(item => (402 <div key={item.id} className="cart-item">403 <span>{item.name}</span>404 <span>¥{item.price}</span>405 <div>406 <button407 onClick={() => dispatch(updateQuantity({ id: item.id, quantity: item.quantity - 1 }))}408 >409 -410 </button>411 <span>{item.quantity}</span>412 <button413 onClick={() => dispatch(updateQuantity({ id: item.id, quantity: item.quantity + 1 }))}414 >415 +416 </button>417 </div>418 <button onClick={() => dispatch(removeItem(item.id))}>419 删除420 </button>421 </div>422 ))}423 424 <div className="cart-summary">425 <p>小计: ¥{totalPrice.toFixed(2)}</p>426 <p>总计: ¥{finalPrice.toFixed(2)}</p>427 <button onClick={() => dispatch(clearCart())}>428 清空购物车429 </button>430 </div>431 </>432 )}433 </div>434 );435};Zustand轻量级状态管理
Zustand是一个小巧、快速、可扩展的状态管理解决方案。
Zustand状态管理实现
typescript
1import { create } from 'zustand';2import { subscribeWithSelector } from 'zustand/middleware';3import { immer } from 'zustand/middleware/immer';4import { persist } from 'zustand/middleware';56// 1. 基础Store7interface CounterState {8 count: number;9 increment: () => void;10 decrement: () => void;11 reset: () => void;12 setCount: (count: number) => void;13}1415export const useCounterStore = create<CounterState>((set) => ({16 count: 0,17 increment: () => set((state) => ({ count: state.count + 1 })),18 decrement: () => set((state) => ({ count: state.count - 1 })),19 reset: () => set({ count: 0 }),20 setCount: (count) => set({ count })21}));2223// 2. 复杂Store with Immer24interface Todo {25 id: string;26 text: string;27 completed: boolean;28 createdAt: number;29}3031interface TodoState {32 todos: Todo[];33 filter: 'all' | 'active' | 'completed';34 loading: boolean;35 error: string | null;36 37 // Actions38 addTodo: (text: string) => void;39 toggleTodo: (id: string) => void;40 deleteTodo: (id: string) => void;41 editTodo: (id: string, text: string) => void;42 setFilter: (filter: 'all' | 'active' | 'completed') => void;43 clearCompleted: () => void;44 loadTodos: () => Promise<void>;45 46 // Computed47 filteredTodos: () => Todo[];48 completedCount: () => number;49 activeCount: () => number;50}5152export const useTodoStore = create<TodoState>()(53 immer(54 subscribeWithSelector((set, get) => ({55 todos: [],56 filter: 'all',57 loading: false,58 error: null,59 60 addTodo: (text: string) =>61 set((state) => {62 state.todos.push({63 id: Math.random().toString(36).substr(2, 9),64 text,65 completed: false,66 createdAt: Date.now()67 });68 }),69 70 toggleTodo: (id: string) =>71 set((state) => {72 const todo = state.todos.find(t => t.id === id);73 if (todo) {74 todo.completed = !todo.completed;75 }76 }),77 78 deleteTodo: (id: string) =>79 set((state) => {80 state.todos = state.todos.filter(t => t.id !== id);81 }),82 83 editTodo: (id: string, text: string) =>84 set((state) => {85 const todo = state.todos.find(t => t.id === id);86 if (todo) {87 todo.text = text;88 }89 }),90 91 setFilter: (filter) => set({ filter }),92 93 clearCompleted: () =>94 set((state) => {95 state.todos = state.todos.filter(t => !t.completed);96 }),97 98 loadTodos: async () => {99 set({ loading: true, error: null });100 101 try {102 const response = await fetch('/api/todos');103 if (!response.ok) {104 throw new Error('Failed to load todos');105 }106 107 const todos = await response.json();108 set({ todos, loading: false });109 } catch (error) {110 set({111 error: error instanceof Error ? error.message : 'Unknown error',112 loading: false113 });114 }115 },116 117 // Computed values118 filteredTodos: () => {119 const { todos, filter } = get();120 switch (filter) {121 case 'active':122 return todos.filter(t => !t.completed);123 case 'completed':124 return todos.filter(t => t.completed);125 default:126 return todos;127 }128 },129 130 completedCount: () => get().todos.filter(t => t.completed).length,131 activeCount: () => get().todos.filter(t => !t.completed).length132 }))133 )134);135136// 3. 持久化Store137interface UserState {138 user: User | null;139 theme: 'light' | 'dark';140 language: 'zh' | 'en';141 preferences: {142 notifications: boolean;143 autoSave: boolean;144 compactMode: boolean;145 };146 147 // Actions148 setUser: (user: User | null) => void;149 toggleTheme: () => void;150 setLanguage: (language: 'zh' | 'en') => void;151 updatePreferences: (preferences: Partial<UserState['preferences']>) => void;152 login: (email: string, password: string) => Promise<void>;153 logout: () => void;154}155156export const useUserStore = create<UserState>()(157 persist(158 immer((set, get) => ({159 user: null,160 theme: 'light',161 language: 'zh',162 preferences: {163 notifications: true,164 autoSave: true,165 compactMode: false166 },167 168 setUser: (user) => set({ user }),169 170 toggleTheme: () =>171 set((state) => {172 state.theme = state.theme === 'light' ? 'dark' : 'light';173 }),174 175 setLanguage: (language) => set({ language }),176 177 updatePreferences: (newPreferences) =>178 set((state) => {179 Object.assign(state.preferences, newPreferences);180 }),181 182 login: async (email: string, password: string) => {183 try {184 const response = await fetch('/api/auth/login', {185 method: 'POST',186 headers: { 'Content-Type': 'application/json' },187 body: JSON.stringify({ email, password })188 });189 190 if (!response.ok) {191 throw new Error('Login failed');192 }193 194 const { user, token } = await response.json();195 localStorage.setItem('token', token);196 set({ user });197 } catch (error) {198 throw error;199 }200 },201 202 logout: () => {203 localStorage.removeItem('token');204 set({ user: null });205 }206 })),207 {208 name: 'user-storage',209 partialize: (state) => ({210 theme: state.theme,211 language: state.language,212 preferences: state.preferences213 })214 }215 )216);217218// 4. 订阅和中间件219// 主题变化监听220useUserStore.subscribe(221 (state) => state.theme,222 (theme) => {223 document.documentElement.setAttribute('data-theme', theme);224 }225);226227// 语言变化监听228useUserStore.subscribe(229 (state) => state.language,230 (language) => {231 document.documentElement.setAttribute('lang', language);232 }233);234235// 5. 选择器优化236export const useUser = () => useUserStore((state) => state.user);237export const useTheme = () => useUserStore((state) => state.theme);238export const useLanguage = () => useUserStore((state) => state.language);239export const usePreferences = () => useUserStore((state) => state.preferences);240241// 浅比较选择器242export const useTodoStats = () => 243 useTodoStore(244 (state) => ({245 total: state.todos.length,246 completed: state.completedCount(),247 active: state.activeCount()248 }),249 (a, b) => a.total === b.total && a.completed === b.completed && a.active === b.active250 );251252// 6. 使用示例253const TodoApp: React.FC = () => {254 const {255 todos,256 filter,257 loading,258 error,259 addTodo,260 toggleTodo,261 deleteTodo,262 setFilter,263 clearCompleted,264 loadTodos,265 filteredTodos266 } = useTodoStore();267 268 const stats = useTodoStats();269 const [newTodoText, setNewTodoText] = useState('');270 271 useEffect(() => {272 loadTodos();273 }, [loadTodos]);274 275 const handleAddTodo = (e: React.FormEvent) => {276 e.preventDefault();277 if (newTodoText.trim()) {278 addTodo(newTodoText.trim());279 setNewTodoText('');280 }281 };282 283 if (loading) return <div>加载中...</div>;284 if (error) return <div>错误: {error}</div>;285 286 return (287 <div className="todo-app">288 <h1>待办事项 ({stats.total})</h1>289 290 <form onSubmit={handleAddTodo}>291 <input292 type="text"293 value={newTodoText}294 onChange={(e) => setNewTodoText(e.target.value)}295 placeholder="添加新的待办事项..."296 />297 <button type="submit">添加</button>298 </form>299 300 <div className="filters">301 <button302 className={filter === 'all' ? 'active' : ''}303 onClick={() => setFilter('all')}304 >305 全部 ({stats.total})306 </button>307 <button308 className={filter === 'active' ? 'active' : ''}309 onClick={() => setFilter('active')}310 >311 未完成 ({stats.active})312 </button>313 <button314 className={filter === 'completed' ? 'active' : ''}315 onClick={() => setFilter('completed')}316 >317 已完成 ({stats.completed})318 </button>319 </div>320 321 <ul className="todo-list">322 {filteredTodos().map(todo => (323 <li key={todo.id} className={todo.completed ? 'completed' : ''}>324 <input325 type="checkbox"326 checked={todo.completed}327 onChange={() => toggleTodo(todo.id)}328 />329 <span>{todo.text}</span>330 <button onClick={() => deleteTodo(todo.id)}>删除</button>331 </li>332 ))}333 </ul>334 335 {stats.completed > 0 && (336 <button onClick={clearCompleted}>337 清除已完成 ({stats.completed})338 </button>339 )}340 </div>341 );342};343344const UserSettings: React.FC = () => {345 const { theme, language, preferences, toggleTheme, setLanguage, updatePreferences } = useUserStore();346 347 return (348 <div className="user-settings">349 <h2>用户设置</h2>350 351 <div className="setting-group">352 <label>主题</label>353 <button onClick={toggleTheme}>354 {theme === 'light' ? '🌙 切换到深色' : '☀️ 切换到浅色'}355 </button>356 </div>357 358 <div className="setting-group">359 <label>语言</label>360 <select value={language} onChange={(e) => setLanguage(e.target.value as 'zh' | 'en')}>361 <option value="zh">中文</option>362 <option value="en">English</option>363 </select>364 </div>365 366 <div className="setting-group">367 <label>368 <input369 type="checkbox"370 checked={preferences.notifications}371 onChange={(e) => updatePreferences({ notifications: e.target.checked })}372 />373 启用通知374 </label>375 </div>376 377 <div className="setting-group">378 <label>379 <input380 type="checkbox"381 checked={preferences.autoSave}382 onChange={(e) => updatePreferences({ autoSave: e.target.checked })}383 />384 自动保存385 </label>386 </div>387 388 <div className="setting-group">389 <label>390 <input391 type="checkbox"392 checked={preferences.compactMode}393 onChange={(e) => updatePreferences({ compactMode: e.target.checked })}394 />395 紧凑模式396 </label>397 </div>398 </div>399 );400};Jotai原子化状态管理
Jotai采用原子化的状态管理方式,提供了更细粒度的状态控制。
Jotai原子化状态管理
typescript
1import { atom, useAtom, useAtomValue, useSetAtom } from 'jotai';2import { atomWithStorage, atomWithReset, RESET } from 'jotai/utils';3import { atomWithImmer } from 'jotai-immer';45// 1. 基础原子6export const countAtom = atom(0);7export const nameAtom = atom('');8export const emailAtom = atom('');910// 2. 派生原子11export const doubleCountAtom = atom((get) => get(countAtom) * 2);12export const isEvenAtom = atom((get) => get(countAtom) % 2 === 0);1314// 3. 可写派生原子15export const uppercaseNameAtom = atom(16 (get) => get(nameAtom).toUpperCase(),17 (get, set, newValue: string) => {18 set(nameAtom, newValue.toLowerCase());19 }20);2122// 4. 异步原子23interface User {24 id: number;25 name: string;26 email: string;27}2829export const userIdAtom = atom<number | null>(null);3031export const userAtom = atom(async (get) => {32 const userId = get(userIdAtom);33 if (!userId) return null;34 35 const response = await fetch(`/api/users/${userId}`);36 if (!response.ok) {37 throw new Error('Failed to fetch user');38 }39 40 return response.json() as Promise<User>;41});4243// 5. 存储原子44export const themeAtom = atomWithStorage<'light' | 'dark'>('theme', 'light');45export const languageAtom = atomWithStorage<'zh' | 'en'>('language', 'zh');4647// 6. 可重置原子48export const formDataAtom = atomWithReset({49 name: '',50 email: '',51 message: ''52});5354// 7. Immer原子55interface Todo {56 id: string;57 text: string;58 completed: boolean;59 createdAt: number;60}6162export const todosAtom = atomWithImmer<Todo[]>([]);6364// 8. 复杂状态组合65interface AppState {66 user: User | null;67 todos: Todo[];68 filter: 'all' | 'active' | 'completed';69 loading: boolean;70 error: string | null;71}7273// 分离的原子74export const currentUserAtom = atom<User | null>(null);75export const todoListAtom = atomWithImmer<Todo[]>([]);76export const todoFilterAtom = atom<'all' | 'active' | 'completed'>('all');77export const loadingAtom = atom(false);78export const errorAtom = atom<string | null>(null);7980// 派生原子81export const filteredTodosAtom = atom((get) => {82 const todos = get(todoListAtom);83 const filter = get(todoFilterAtom);84 85 switch (filter) {86 case 'active':87 return todos.filter(todo => !todo.completed);88 case 'completed':89 return todos.filter(todo => todo.completed);90 default:91 return todos;92 }93});9495export const todoStatsAtom = atom((get) => {96 const todos = get(todoListAtom);97 return {98 total: todos.length,99 completed: todos.filter(t => t.completed).length,100 active: todos.filter(t => !t.completed).length101 };102});103104// 9. 动作原子105export const addTodoAtom = atom(106 null,107 (get, set, text: string) => {108 const newTodo: Todo = {109 id: Math.random().toString(36).substr(2, 9),110 text,111 completed: false,112 createdAt: Date.now()113 };114 115 set(todoListAtom, (draft) => {116 draft.push(newTodo);117 });118 }119);120121export const toggleTodoAtom = atom(122 null,123 (get, set, id: string) => {124 set(todoListAtom, (draft) => {125 const todo = draft.find(t => t.id === id);126 if (todo) {127 todo.completed = !todo.completed;128 }129 });130 }131);132133export const deleteTodoAtom = atom(134 null,135 (get, set, id: string) => {136 set(todoListAtom, (draft) => {137 const index = draft.findIndex(t => t.id === id);138 if (index !== -1) {139 draft.splice(index, 1);140 }141 });142 }143);144145export const clearCompletedAtom = atom(146 null,147 (get, set) => {148 set(todoListAtom, (draft) => {149 return draft.filter(t => !t.completed);150 });151 }152);153154// 10. 异步动作原子155export const loadTodosAtom = atom(156 null,157 async (get, set) => {158 set(loadingAtom, true);159 set(errorAtom, null);160 161 try {162 const response = await fetch('/api/todos');163 if (!response.ok) {164 throw new Error('Failed to load todos');165 }166 167 const todos = await response.json();168 set(todoListAtom, todos);169 } catch (error) {170 set(errorAtom, error instanceof Error ? error.message : 'Unknown error');171 } finally {172 set(loadingAtom, false);173 }174 }175);176177export const saveTodoAtom = atom(178 null,179 async (get, set, todo: Todo) => {180 try {181 const response = await fetch(`/api/todos/${todo.id}`, {182 method: 'PUT',183 headers: { 'Content-Type': 'application/json' },184 body: JSON.stringify(todo)185 });186 187 if (!response.ok) {188 throw new Error('Failed to save todo');189 }190 191 const updatedTodo = await response.json();192 193 set(todoListAtom, (draft) => {194 const index = draft.findIndex(t => t.id === todo.id);195 if (index !== -1) {196 draft[index] = updatedTodo;197 }198 });199 } catch (error) {200 set(errorAtom, error instanceof Error ? error.message : 'Save failed');201 }202 }203);204205// 11. 使用示例组件206const Counter: React.FC = () => {207 const [count, setCount] = useAtom(countAtom);208 const doubleCount = useAtomValue(doubleCountAtom);209 const isEven = useAtomValue(isEvenAtom);210 211 return (212 <div className="counter">213 <h2>计数器</h2>214 <p>当前值: {count}</p>215 <p>双倍值: {doubleCount}</p>216 <p>是否为偶数: {isEven ? '是' : '否'}</p>217 218 <div className="controls">219 <button onClick={() => setCount(c => c - 1)}>-1</button>220 <button onClick={() => setCount(c => c + 1)}>+1</button>221 <button onClick={() => setCount(0)}>重置</button>222 </div>223 </div>224 );225};226227const TodoApp: React.FC = () => {228 const todos = useAtomValue(filteredTodosAtom);229 const stats = useAtomValue(todoStatsAtom);230 const [filter, setFilter] = useAtom(todoFilterAtom);231 const loading = useAtomValue(loadingAtom);232 const error = useAtomValue(errorAtom);233 234 const addTodo = useSetAtom(addTodoAtom);235 const toggleTodo = useSetAtom(toggleTodoAtom);236 const deleteTodo = useSetAtom(deleteTodoAtom);237 const clearCompleted = useSetAtom(clearCompletedAtom);238 const loadTodos = useSetAtom(loadTodosAtom);239 240 const [newTodoText, setNewTodoText] = useState('');241 242 useEffect(() => {243 loadTodos();244 }, [loadTodos]);245 246 const handleAddTodo = (e: React.FormEvent) => {247 e.preventDefault();248 if (newTodoText.trim()) {249 addTodo(newTodoText.trim());250 setNewTodoText('');251 }252 };253 254 if (loading) return <div>加载中...</div>;255 if (error) return <div>错误: {error}</div>;256 257 return (258 <div className="todo-app">259 <h1>待办事项 ({stats.total})</h1>260 261 <form onSubmit={handleAddTodo}>262 <input263 type="text"264 value={newTodoText}265 onChange={(e) => setNewTodoText(e.target.value)}266 placeholder="添加新的待办事项..."267 />268 <button type="submit">添加</button>269 </form>270 271 <div className="filters">272 <button273 className={filter === 'all' ? 'active' : ''}274 onClick={() => setFilter('all')}275 >276 全部 ({stats.total})277 </button>278 <button279 className={filter === 'active' ? 'active' : ''}280 onClick={() => setFilter('active')}281 >282 未完成 ({stats.active})283 </button>284 <button285 className={filter === 'completed' ? 'active' : ''}286 onClick={() => setFilter('completed')}287 >288 已完成 ({stats.completed})289 </button>290 </div>291 292 <ul className="todo-list">293 {todos.map(todo => (294 <TodoItem295 key={todo.id}296 todo={todo}297 onToggle={() => toggleTodo(todo.id)}298 onDelete={() => deleteTodo(todo.id)}299 />300 ))}301 </ul>302 303 {stats.completed > 0 && (304 <button onClick={() => clearCompleted()}>305 清除已完成 ({stats.completed})306 </button>307 )}308 </div>309 );310};311312const TodoItem: React.FC<{313 todo: Todo;314 onToggle: () => void;315 onDelete: () => void;316}> = ({ todo, onToggle, onDelete }) => {317 const [editing, setEditing] = useState(false);318 const [text, setText] = useState(todo.text);319 const saveTodo = useSetAtom(saveTodoAtom);320 321 const handleSave = async () => {322 if (text.trim() && text !== todo.text) {323 await saveTodo({ ...todo, text: text.trim() });324 }325 setEditing(false);326 };327 328 const handleCancel = () => {329 setText(todo.text);330 setEditing(false);331 };332 333 return (334 <li className={`todo-item ${todo.completed ? 'completed' : ''}`}>335 <input336 type="checkbox"337 checked={todo.completed}338 onChange={onToggle}339 />340 341 {editing ? (342 <div className="edit-mode">343 <input344 type="text"345 value={text}346 onChange={(e) => setText(e.target.value)}347 onKeyDown={(e) => {348 if (e.key === 'Enter') handleSave();349 if (e.key === 'Escape') handleCancel();350 }}351 autoFocus352 />353 <button onClick={handleSave}>保存</button>354 <button onClick={handleCancel}>取消</button>355 </div>356 ) : (357 <div className="view-mode">358 <span onDoubleClick={() => setEditing(true)}>359 {todo.text}360 </span>361 <button onClick={() => setEditing(true)}>编辑</button>362 <button onClick={onDelete}>删除</button>363 </div>364 )}365 </li>366 );367};368369// 12. 表单处理370const ContactForm: React.FC = () => {371 const [formData, setFormData] = useAtom(formDataAtom);372 const resetForm = useSetAtom(RESET);373 374 const handleSubmit = async (e: React.FormEvent) => {375 e.preventDefault();376 377 try {378 const response = await fetch('/api/contact', {379 method: 'POST',380 headers: { 'Content-Type': 'application/json' },381 body: JSON.stringify(formData)382 });383 384 if (response.ok) {385 alert('消息发送成功!');386 resetForm(formDataAtom);387 } else {388 throw new Error('发送失败');389 }390 } catch (error) {391 alert('发送失败,请重试');392 }393 };394 395 return (396 <form onSubmit={handleSubmit} className="contact-form">397 <h2>联系我们</h2>398 399 <div className="form-field">400 <label htmlFor="name">姓名</label>401 <input402 id="name"403 type="text"404 value={formData.name}405 onChange={(e) => setFormData(prev => ({ ...prev, name: e.target.value }))}406 required407 />408 </div>409 410 <div className="form-field">411 <label htmlFor="email">邮箱</label>412 <input413 id="email"414 type="email"415 value={formData.email}416 onChange={(e) => setFormData(prev => ({ ...prev, email: e.target.value }))}417 required418 />419 </div>420 421 <div className="form-field">422 <label htmlFor="message">消息</label>423 <textarea424 id="message"425 value={formData.message}426 onChange={(e) => setFormData(prev => ({ ...prev, message: e.target.value }))}427 required428 rows={4}429 />430 </div>431 432 <div className="form-actions">433 <button type="submit">发送</button>434 <button type="button" onClick={() => resetForm(formDataAtom)}>435 重置436 </button>437 </div>438 </form>439 );440};
参与讨论