react中redux的使用详细说明
Redux 使用指南
目录
- Redux 简介
- 核心概念
- 安装和基础配置
- Actions
- Reducers
- Store
- React-Redux 集成
- Redux Toolkit (推荐)
- 异步操作
- 中间件
- 完整实战示例
- 最佳实践
Redux 简介
Redux 是一个用于 JavaScript 应用程序的可预测状态容器。它帮助你编写行为一致、运行在不同环境(客户端、服务器和原生应用)中、易于测试的应用程序。
为什么使用 Redux?
- 可预测性:状态变化是可预测的,因为它们是纯函数的结果
- 集中化:应用的状态被存储在单一的 store 中
- 可调试:Redux DevTools 提供强大的调试功能
- 灵活性:可以与任何 UI 层一起使用
核心概念
Redux 基于三个核心原则:
1. 单一数据源
整个应用的 state 被储存在一棵 object tree 中,并且这个 object tree 只存在于唯一一个 store 中。
2. State 是只读的
唯一改变 state 的方法就是触发 action,action 是一个用于描述已发生事件的普通对象。
3. 使用纯函数来执行修改
为了描述 action 如何改变 state tree,你需要编写 reducers。
Redux 数据流
UI → Action → Reducer → Store → UI
- UI 触发一个 Action
- Action 被发送到 Reducer
- Reducer 根据 Action 更新 State
- Store 通知 UI 状态已更新
- UI 重新渲染
安装和基础配置
安装 Redux
# 使用 npm
npm install redux react-redux# 使用 yarn
yarn add redux react-redux# 如果使用 TypeScript
npm install @types/react-redux
安装 Redux Toolkit (推荐)
# Redux Toolkit 是官方推荐的方式
npm install @reduxjs/toolkit react-redux
Actions
Action 是把数据从应用传到 store 的有效载荷。它们是 store 数据的唯一来源。
Action 的基本结构
// 基本的 action
const ADD_TODO = 'ADD_TODO';// Action Creator
const addTodo = (text) => ({type: ADD_TODO,payload: {id: Date.now(),text,completed: false}
});// 使用
const action = addTodo('学习 Redux');
console.log(action);
// { type: 'ADD_TODO', payload: { id: 1642345678901, text: '学习 Redux', completed: false } }
复杂的 Action 示例
// actions/todoActions.js// Action Types
export const ADD_TODO = 'ADD_TODO';
export const TOGGLE_TODO = 'TOGGLE_TODO';
export const DELETE_TODO = 'DELETE_TODO';
export const SET_FILTER = 'SET_FILTER';
export const FETCH_TODOS_REQUEST = 'FETCH_TODOS_REQUEST';
export const FETCH_TODOS_SUCCESS = 'FETCH_TODOS_SUCCESS';
export const FETCH_TODOS_FAILURE = 'FETCH_TODOS_FAILURE';// Action Creators
export const addTodo = (text) => ({type: ADD_TODO,payload: {id: Date.now(),text,completed: false,createdAt: new Date().toISOString()}
});export const toggleTodo = (id) => ({type: TOGGLE_TODO,payload: { id }
});export const deleteTodo = (id) => ({type: DELETE_TODO,payload: { id }
});export const setFilter = (filter) => ({type: SET_FILTER,payload: { filter }
});// 异步 Action Creator (需要 redux-thunk)
export const fetchTodos = () => {return async (dispatch) => {dispatch({ type: FETCH_TODOS_REQUEST });try {const response = await fetch('/api/todos');const todos = await response.json();dispatch({type: FETCH_TODOS_SUCCESS,payload: { todos }});} catch (error) {dispatch({type: FETCH_TODOS_FAILURE,payload: { error: error.message }});}};
};
Reducers
Reducer 指定了应用状态的变化如何响应 actions 并发送到 store 的。
基本 Reducer
// reducers/todoReducer.js
import {ADD_TODO,TOGGLE_TODO,DELETE_TODO,SET_FILTER,FETCH_TODOS_REQUEST,FETCH_TODOS_SUCCESS,FETCH_TODOS_FAILURE
} from '../actions/todoActions';// 初始状态
const initialState = {todos: [],filter: 'ALL', // ALL, ACTIVE, COMPLETEDloading: false,error: null
};// Reducer 函数
const todoReducer = (state = initialState, action) => {switch (action.type) {case ADD_TODO:return {...state,todos: [...state.todos, action.payload]};case TOGGLE_TODO:return {...state,todos: state.todos.map(todo =>todo.id === action.payload.id? { ...todo, completed: !todo.completed }: todo)};case DELETE_TODO:return {...state,todos: state.todos.filter(todo => todo.id !== action.payload.id)};case SET_FILTER:return {...state,filter: action.payload.filter};case FETCH_TODOS_REQUEST:return {...state,loading: true,error: null};case FETCH_TODOS_SUCCESS:return {...state,loading: false,todos: action.payload.todos};case FETCH_TODOS_FAILURE:return {...state,loading: false,error: action.payload.error};default:return state;}
};export default todoReducer;
组合 Reducers
// reducers/index.js
import { combineReducers } from 'redux';
import todoReducer from './todoReducer';
import userReducer from './userReducer';
import uiReducer from './uiReducer';const rootReducer = combineReducers({todos: todoReducer,user: userReducer,ui: uiReducer
});export default rootReducer;
用户 Reducer 示例
// reducers/userReducer.js
const initialState = {currentUser: null,isAuthenticated: false,loading: false,error: null
};const userReducer = (state = initialState, action) => {switch (action.type) {case 'LOGIN_REQUEST':return {...state,loading: true,error: null};case 'LOGIN_SUCCESS':return {...state,loading: false,currentUser: action.payload.user,isAuthenticated: true};case 'LOGIN_FAILURE':return {...state,loading: false,error: action.payload.error,isAuthenticated: false};case 'LOGOUT':return {...state,currentUser: null,isAuthenticated: false};default:return state;}
};export default userReducer;
Store
Store 是把 actions 和 reducers 联系到一起的对象。
创建 Store
// store/index.js
import { createStore, applyMiddleware, compose } from 'redux';
import thunk from 'redux-thunk';
import rootReducer from '../reducers';// 配置 Redux DevTools
const composeEnhancers = typeof window === 'object' && window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__? window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__({// 指定扩展的配置}): compose;// 创建 store
const store = createStore(rootReducer,composeEnhancers(applyMiddleware(thunk))
);export default store;
Store 的基本使用
import store from './store';
import { addTodo, toggleTodo } from './actions/todoActions';// 获取当前状态
console.log(store.getState());// 订阅状态变化
const unsubscribe = store.subscribe(() => {console.log('State changed:', store.getState());
});// 派发 actions
store.dispatch(addTodo('学习 Redux'));
store.dispatch(addTodo('构建应用'));
store.dispatch(toggleTodo(1));// 取消订阅
unsubscribe();
React-Redux 集成
React-Redux 是 Redux 的官方 React 绑定库。
Provider 组件
// index.js 或 App.js
import React from 'react';
import ReactDOM from 'react-dom';
import { Provider } from 'react-redux';
import store from './store';
import App from './App';ReactDOM.render(<Provider store={store}><App /></Provider>,document.getElementById('root')
);
使用 useSelector 和 useDispatch Hooks
// components/TodoList.js
import React from 'react';
import { useSelector, useDispatch } from 'react-redux';
import { addTodo, toggleTodo, deleteTodo, setFilter } from '../actions/todoActions';const TodoList = () => {const dispatch = useDispatch();// 使用 useSelector 获取状态const { todos, filter, loading, error } = useSelector(state => state.todos);const [inputValue, setInputValue] = React.useState('');// 过滤 todosconst filteredTodos = todos.filter(todo => {switch (filter) {case 'ACTIVE':return !todo.completed;case 'COMPLETED':return todo.completed;default:return true;}});const handleAddTodo = (e) => {e.preventDefault();if (inputValue.trim()) {dispatch(addTodo(inputValue.trim()));setInputValue('');}};const handleToggleTodo = (id) => {dispatch(toggleTodo(id));};const handleDeleteTodo = (id) => {dispatch(deleteTodo(id));};const handleFilterChange = (newFilter) => {dispatch(setFilter(newFilter));};if (loading) return <div>加载中...</div>;if (error) return <div>错误: {error}</div>;return (<div className="todo-app"><h1>Todo List</h1>{/* 添加 Todo 表单 */}<form onSubmit={handleAddTodo}><inputtype="text"value={inputValue}onChange={(e) => setInputValue(e.target.value)}placeholder="添加新任务..."/><button type="submit">添加</button></form>{/* 过滤器 */}<div className="filters"><button className={filter === 'ALL' ? 'active' : ''}onClick={() => handleFilterChange('ALL')}>全部</button><button className={filter === 'ACTIVE' ? 'active' : ''}onClick={() => handleFilterChange('ACTIVE')}>未完成</button><button className={filter === 'COMPLETED' ? 'active' : ''}onClick={() => handleFilterChange('COMPLETED')}>已完成</button></div>{/* Todo 列表 */}<ul className="todo-list">{filteredTodos.map(todo => (<li key={todo.id} className={todo.completed ? 'completed' : ''}><inputtype="checkbox"checked={todo.completed}onChange={() => handleToggleTodo(todo.id)}/><span>{todo.text}</span><button onClick={() => handleDeleteTodo(todo.id)}>删除</button></li>))}</ul>{/* 统计信息 */}<div className="stats"><span>总计: {todos.length}</span><span>未完成: {todos.filter(t => !t.completed).length}</span><span>已完成: {todos.filter(t => t.completed).length}</span></div></div>);
};export default TodoList;
使用 connect (类组件方式)
// components/TodoListClass.js
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { addTodo, toggleTodo, deleteTodo } from '../actions/todoActions';class TodoListClass extends Component {constructor(props) {super(props);this.state = {inputValue: ''};}handleAddTodo = (e) => {e.preventDefault();if (this.state.inputValue.trim()) {this.props.addTodo(this.state.inputValue.trim());this.setState({ inputValue: '' });}};render() {const { todos, toggleTodo, deleteTodo } = this.props;const { inputValue } = this.state;return (<div><form onSubmit={this.handleAddTodo}><inputtype="text"value={inputValue}onChange={(e) => this.setState({ inputValue: e.target.value })}placeholder="添加新任务..."/><button type="submit">添加</button></form><ul>{todos.map(todo => (<li key={todo.id}><inputtype="checkbox"checked={todo.completed}onChange={() => toggleTodo(todo.id)}/><span>{todo.text}</span><button onClick={() => deleteTodo(todo.id)}>删除</button></li>))}</ul></div>);}
}// mapStateToProps
const mapStateToProps = (state) => ({todos: state.todos.todos
});// mapDispatchToProps
const mapDispatchToProps = {addTodo,toggleTodo,deleteTodo
};export default connect(mapStateToProps, mapDispatchToProps)(TodoListClass);
Redux Toolkit (推荐)
Redux Toolkit 是官方推荐的编写 Redux 逻辑的方法,它简化了 Redux 的使用。
安装
npm install @reduxjs/toolkit react-redux
使用 createSlice
// features/todoSlice.js
import { createSlice, createAsyncThunk } from '@reduxjs/toolkit';// 异步 thunk
export const fetchTodos = createAsyncThunk('todos/fetchTodos',async (_, { rejectWithValue }) => {try {const response = await fetch('/api/todos');if (!response.ok) {throw new Error('Failed to fetch todos');}return await response.json();} catch (error) {return rejectWithValue(error.message);}}
);export const addTodoAsync = createAsyncThunk('todos/addTodoAsync',async (todoText, { rejectWithValue }) => {try {const response = await fetch('/api/todos', {method: 'POST',headers: {'Content-Type': 'application/json',},body: JSON.stringify({ text: todoText }),});if (!response.ok) {throw new Error('Failed to add todo');}return await response.json();} catch (error) {return rejectWithValue(error.message);}}
);const todoSlice = createSlice({name: 'todos',initialState: {items: [],filter: 'ALL',loading: false,error: null},reducers: {addTodo: (state, action) => {state.items.push({id: Date.now(),text: action.payload,completed: false,createdAt: new Date().toISOString()});},toggleTodo: (state, action) => {const todo = state.items.find(todo => todo.id === action.payload);if (todo) {todo.completed = !todo.completed;}},deleteTodo: (state, action) => {state.items = state.items.filter(todo => todo.id !== action.payload);},setFilter: (state, action) => {state.filter = action.payload;},clearCompleted: (state) => {state.items = state.items.filter(todo => !todo.completed);}},extraReducers: (builder) => {builder// fetchTodos.addCase(fetchTodos.pending, (state) => {state.loading = true;state.error = null;}).addCase(fetchTodos.fulfilled, (state, action) => {state.loading = false;state.items = action.payload;}).addCase(fetchTodos.rejected, (state, action) => {state.loading = false;state.error = action.payload;})// addTodoAsync.addCase(addTodoAsync.pending, (state) => {state.loading = true;}).addCase(addTodoAsync.fulfilled, (state, action) => {state.loading = false;state.items.push(action.payload);}).addCase(addTodoAsync.rejected, (state, action) => {state.loading = false;state.error = action.payload;});}
});export const { addTodo, toggleTodo, deleteTodo, setFilter, clearCompleted } = todoSlice.actions;
export default todoSlice.reducer;
配置 Store
// store/index.js
import { configureStore } from '@reduxjs/toolkit';
import todoReducer from '../features/todoSlice';
import userReducer from '../features/userSlice';export const store = configureStore({reducer: {todos: todoReducer,user: userReducer},middleware: (getDefaultMiddleware) =>getDefaultMiddleware({serializableCheck: {ignoredActions: ['persist/PERSIST']}}),devTools: process.env.NODE_ENV !== 'production'
});export type RootState = ReturnType<typeof store.getState>;
export type AppDispatch = typeof store.dispatch;
TypeScript 支持
// hooks/redux.ts
import { useDispatch, useSelector, TypedUseSelectorHook } from 'react-redux';
import type { RootState, AppDispatch } from '../store';export const useAppDispatch = () => useDispatch<AppDispatch>();
export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector;
// components/TodoListTS.tsx
import React, { useState, useEffect } from 'react';
import { useAppDispatch, useAppSelector } from '../hooks/redux';
import { addTodo, toggleTodo, deleteTodo, setFilter, fetchTodos,clearCompleted
} from '../features/todoSlice';const TodoListTS: React.FC = () => {const dispatch = useAppDispatch();const { items: todos, filter, loading, error } = useAppSelector(state => state.todos);const [inputValue, setInputValue] = useState('');useEffect(() => {dispatch(fetchTodos());}, [dispatch]);const filteredTodos = todos.filter(todo => {switch (filter) {case 'ACTIVE':return !todo.completed;case 'COMPLETED':return todo.completed;default:return true;}});const handleAddTodo = (e: React.FormEvent) => {e.preventDefault();if (inputValue.trim()) {dispatch(addTodo(inputValue.trim()));setInputValue('');}};return (<div className="todo-app"><h1>Todo List (TypeScript)</h1><form onSubmit={handleAddTodo}><inputtype="text"value={inputValue}onChange={(e) => setInputValue(e.target.value)}placeholder="添加新任务..."disabled={loading}/><button type="submit" disabled={loading}>{loading ? '添加中...' : '添加'}</button></form><div className="filters">{(['ALL', 'ACTIVE', 'COMPLETED'] as const).map(filterType => (<buttonkey={filterType}className={filter === filterType ? 'active' : ''}onClick={() => dispatch(setFilter(filterType))}>{filterType === 'ALL' ? '全部' : filterType === 'ACTIVE' ? '未完成' : '已完成'}</button>))}</div>{error && <div className="error">错误: {error}</div>}<ul className="todo-list">{filteredTodos.map(todo => (<li key={todo.id} className={todo.completed ? 'completed' : ''}><inputtype="checkbox"checked={todo.completed}onChange={() => dispatch(toggleTodo(todo.id))}/><span>{todo.text}</span><button onClick={() => dispatch(deleteTodo(todo.id))}>删除</button></li>))}</ul><div className="actions"><button onClick={() => dispatch(clearCompleted())}disabled={!todos.some(todo => todo.completed)}>清除已完成</button></div></div>);
};export default TodoListTS;
异步操作
使用 Redux Thunk
// actions/asyncActions.js
export const fetchUserProfile = (userId) => {return async (dispatch, getState) => {dispatch({ type: 'FETCH_USER_PROFILE_REQUEST' });try {const response = await fetch(`/api/users/${userId}`);const user = await response.json();dispatch({type: 'FETCH_USER_PROFILE_SUCCESS',payload: user});// 可以访问当前状态const currentState = getState();console.log('Current state:', currentState);} catch (error) {dispatch({type: 'FETCH_USER_PROFILE_FAILURE',payload: error.message});}};
};// 条件派发
export const fetchUserIfNeeded = (userId) => {return (dispatch, getState) => {const { user } = getState();if (!user.profiles[userId] || user.profiles[userId].stale) {return dispatch(fetchUserProfile(userId));}};
};
使用 createAsyncThunk (Redux Toolkit)
// features/userSlice.js
import { createSlice, createAsyncThunk } from '@reduxjs/toolkit';// 异步 thunk
export const fetchUserProfile = createAsyncThunk('user/fetchProfile',async (userId, { getState, rejectWithValue }) => {try {const state = getState();// 检查是否已经有数据if (state.user.profiles[userId] && !state.user.profiles[userId].stale) {return state.user.profiles[userId];}const response = await fetch(`/api/users/${userId}`);if (!response.ok) {throw new Error(`HTTP error! status: ${response.status}`);}const userData = await response.json();return { userId, ...userData };} catch (error) {return rejectWithValue(error.message);}}
);export const updateUserProfile = createAsyncThunk('user/updateProfile',async ({ userId, updates }, { rejectWithValue }) => {try {const response = await fetch(`/api/users/${userId}`, {method: 'PUT',headers: {'Content-Type': 'application/json',},body: JSON.stringify(updates),});if (!response.ok) {throw new Error('Failed to update profile');}return await response.json();} catch (error) {return rejectWithValue(error.message);}}
);const userSlice = createSlice({name: 'user',initialState: {profiles: {},currentUserId: null,loading: false,error: null},reducers: {setCurrentUser: (state, action) => {state.currentUserId = action.payload;},clearError: (state) => {state.error = null;}},extraReducers: (builder) => {builder.addCase(fetchUserProfile.pending, (state) => {state.loading = true;state.error = null;}).addCase(fetchUserProfile.fulfilled, (state, action) => {state.loading = false;const { userId, ...userData } = action.payload;state.profiles[userId] = {...userData,stale: false,lastUpdated: Date.now()};}).addCase(fetchUserProfile.rejected, (state, action) => {state.loading = false;state.error = action.payload;}).addCase(updateUserProfile.fulfilled, (state, action) => {const { id, ...updates } = action.payload;if (state.profiles[id]) {state.profiles[id] = {...state.profiles[id],...updates,lastUpdated: Date.now()};}});}
});export const { setCurrentUser, clearError } = userSlice.actions;
export default userSlice.reducer;
中间件
自定义中间件
// middleware/logger.js
const logger = (store) => (next) => (action) => {console.group(action.type);console.info('dispatching', action);console.log('prev state', store.getState());const result = next(action);console.log('next state', store.getState());console.groupEnd();return result;
};export default logger;
// middleware/crashReporter.js
const crashReporter = (store) => (next) => (action) => {try {return next(action);} catch (err) {console.error('Caught an exception!', err);// 发送错误报告到服务器fetch('/api/errors', {method: 'POST',headers: {'Content-Type': 'application/json',},body: JSON.stringify({error: err.message,stack: err.stack,action,state: store.getState()})});throw err;}
};export default crashReporter;
应用中间件
// store/index.js
import { createStore, applyMiddleware, compose } from 'redux';
import thunk from 'redux-thunk';
import logger from '../middleware/logger';
import crashReporter from '../middleware/crashReporter';
import rootReducer from '../reducers';const middleware = [thunk];if (process.env.NODE_ENV === 'development') {middleware.push(logger);
}middleware.push(crashReporter);const composeEnhancers = typeof window === 'object' && window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__? window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__({}): compose;const store = createStore(rootReducer,composeEnhancers(applyMiddleware(...middleware))
);export default store;
完整实战示例
让我们创建一个完整的购物车应用示例:
项目结构
src/
├── components/
│ ├── ProductList.js
│ ├── Cart.js
│ └── Header.js
├── features/
│ ├── productsSlice.js
│ └── cartSlice.js
├── store/
│ └── index.js
├── hooks/
│ └── redux.js
└── App.js
Products Slice
// features/productsSlice.js
import { createSlice, createAsyncThunk } from '@reduxjs/toolkit';export const fetchProducts = createAsyncThunk('products/fetchProducts',async (_, { rejectWithValue }) => {try {const response = await fetch('/api/products');if (!response.ok) {throw new Error('Failed to fetch products');}return await response.json();} catch (error) {return rejectWithValue(error.message);}}
);const productsSlice = createSlice({name: 'products',initialState: {items: [],loading: false,error: null,categories: []},reducers: {setCategories: (state, action) => {state.categories = action.payload;}},extraReducers: (builder) => {builder.addCase(fetchProducts.pending, (state) => {state.loading = true;state.error = null;}).addCase(fetchProducts.fulfilled, (state, action) => {state.loading = false;state.items = action.payload;// 提取分类const categories = [...new Set(action.payload.map(p => p.category))];state.categories = categories;}).addCase(fetchProducts.rejected, (state, action) => {state.loading = false;state.error = action.payload;});}
});export const { setCategories } = productsSlice.actions;
export default productsSlice.reducer;
Cart Slice
// features/cartSlice.js
import { createSlice } from '@reduxjs/toolkit';const cartSlice = createSlice({name: 'cart',initialState: {items: [],total: 0,itemCount: 0,isOpen: false},reducers: {addToCart: (state, action) => {const { id, name, price, image } = action.payload;const existingItem = state.items.find(item => item.id === id);if (existingItem) {existingItem.quantity += 1;} else {state.items.push({id,name,price,image,quantity: 1});}cartSlice.caseReducers.calculateTotals(state);},removeFromCart: (state, action) => {const id = action.payload;state.items = state.items.filter(item => item.id !== id);cartSlice.caseReducers.calculateTotals(state);},updateQuantity: (state, action) => {const { id, quantity } = action.payload;const item = state.items.find(item => item.id === id);if (item) {item.quantity = Math.max(0, quantity);if (item.quantity === 0) {state.items = state.items.filter(item => item.id !== id);}}cartSlice.caseReducers.calculateTotals(state);},clearCart: (state) => {state.items = [];state.total = 0;state.itemCount = 0;},toggleCart: (state) => {state.isOpen = !state.isOpen;},calculateTotals: (state) => {state.itemCount = state.items.reduce((total, item) => total + item.quantity, 0);state.total = state.items.reduce((total, item) => total + (item.price * item.quantity), 0);}}
});export const { addToCart, removeFromCart, updateQuantity, clearCart, toggleCart
} = cartSlice.actions;export default cartSlice.reducer;
Store 配置
// store/index.js
import { configureStore } from '@reduxjs/toolkit';
import productsReducer from '../features/productsSlice';
import cartReducer from '../features/cartSlice';export const store = configureStore({reducer: {products: productsReducer,cart: cartReducer},middleware: (getDefaultMiddleware) =>getDefaultMiddleware({serializableCheck: {ignoredActions: ['persist/PERSIST']}})
});export type RootState = ReturnType<typeof store.getState>;
export type AppDispatch = typeof store.dispatch;
产品列表组件
// components/ProductList.js
import React, { useEffect, useState } from 'react';
import { useSelector, useDispatch } from 'react-redux';
import { fetchProducts } from '../features/productsSlice';
import { addToCart } from '../features/cartSlice';const ProductList = () => {const dispatch = useDispatch();const { items: products, loading, error, categories } = useSelector(state => state.products);const [selectedCategory, setSelectedCategory] = useState('all');const [searchTerm, setSearchTerm] = useState('');useEffect(() => {dispatch(fetchProducts());}, [dispatch]);const filteredProducts = products.filter(product => {const matchesCategory = selectedCategory === 'all' || product.category === selectedCategory;const matchesSearch = product.name.toLowerCase().includes(searchTerm.toLowerCase());return matchesCategory && matchesSearch;});const handleAddToCart = (product) => {dispatch(addToCart(product));};if (loading) return <div className="loading">加载中...</div>;if (error) return <div className="error">错误: {error}</div>;return (<div className="product-list"><div className="filters"><inputtype="text"placeholder="搜索产品..."value={searchTerm}onChange={(e) => setSearchTerm(e.target.value)}className="search-input"/><select value={selectedCategory} onChange={(e) => setSelectedCategory(e.target.value)}className="category-select"><option value="all">所有分类</option>{categories.map(category => (<option key={category} value={category}>{category}</option>))}</select></div><div className="products-grid">{filteredProducts.map(product => (<div key={product.id} className="product-card"><img src={product.image} alt={product.name} /><h3>{product.name}</h3><p className="description">{product.description}</p><div className="price">¥{product.price}</div><button onClick={() => handleAddToCart(product)}className="add-to-cart-btn">加入购物车</button></div>))}</div></div>);
};export default ProductList;
购物车组件
// components/Cart.js
import React from 'react';
import { useSelector, useDispatch } from 'react-redux';
import { removeFromCart, updateQuantity, clearCart, toggleCart
} from '../features/cartSlice';const Cart = () => {const dispatch = useDispatch();const { items, total, itemCount, isOpen } = useSelector(state => state.cart);const handleQuantityChange = (id, quantity) => {dispatch(updateQuantity({ id, quantity }));};const handleRemoveItem = (id) => {dispatch(removeFromCart(id));};const handleClearCart = () => {if (window.confirm('确定要清空购物车吗?')) {dispatch(clearCart());}};const handleCheckout = () => {alert(`结账总额: ¥${total.toFixed(2)}`);// 这里可以集成支付逻辑};return (<><div className={`cart-overlay ${isOpen ? 'open' : ''}`} onClick={() => dispatch(toggleCart())} /><div className={`cart ${isOpen ? 'open' : ''}`}><div className="cart-header"><h2>购物车 ({itemCount})</h2><button onClick={() => dispatch(toggleCart())} className="close-btn">×</button></div><div className="cart-items">{items.length === 0 ? (<p className="empty-cart">购物车为空</p>) : (items.map(item => (<div key={item.id} className="cart-item"><img src={item.image} alt={item.name} /><div className="item-details"><h4>{item.name}</h4><p>¥{item.price}</p></div><div className="quantity-controls"><button onClick={() => handleQuantityChange(item.id, item.quantity - 1)}disabled={item.quantity <= 1}>-</button><span>{item.quantity}</span><button onClick={() => handleQuantityChange(item.id, item.quantity + 1)}>+</button></div><div className="item-total">¥{(item.price * item.quantity).toFixed(2)}</div><button onClick={() => handleRemoveItem(item.id)}className="remove-btn">删除</button></div>)))}</div>{items.length > 0 && (<div className="cart-footer"><div className="cart-total"><strong>总计: ¥{total.toFixed(2)}</strong></div><div className="cart-actions"><button onClick={handleClearCart} className="clear-btn">清空购物车</button><button onClick={handleCheckout} className="checkout-btn">结账</button></div></div>)}</div></>);
};export default Cart;
头部组件
// components/Header.js
import React from 'react';
import { useSelector, useDispatch } from 'react-redux';
import { toggleCart } from '../features/cartSlice';const Header = () => {const dispatch = useDispatch();const { itemCount } = useSelector(state => state.cart);return (<header className="header"><div className="container"><h1 className="logo">Redux 商店</h1><nav className="nav"><button onClick={() => dispatch(toggleCart())}className="cart-button">购物车 ({itemCount})</button></nav></div></header>);
};export default Header;
主应用组件
// App.js
import React from 'react';
import { Provider } from 'react-redux';
import { store } from './store';
import Header from './components/Header';
import ProductList from './components/ProductList';
import Cart from './components/Cart';
import './App.css';function App() {return (<Provider store={store}><div className="App"><Header /><main className="main"><ProductList /></main><Cart /></div></Provider>);
}export default App;
最佳实践
1. 状态结构设计
// 好的状态结构
const goodState = {entities: {users: {byId: {1: { id: 1, name: 'John', email: 'john@example.com' },2: { id: 2, name: 'Jane', email: 'jane@example.com' }},allIds: [1, 2]},posts: {byId: {101: { id: 101, title: 'Post 1', authorId: 1 },102: { id: 102, title: 'Post 2', authorId: 2 }},allIds: [101, 102]}},ui: {loading: false,error: null,selectedUserId: 1}
};// 避免的状态结构
const badState = {users: [{ id: 1, name: 'John', posts: [{ id: 101, title: 'Post 1' }] }]
};
2. 使用 Selectors
// selectors/todoSelectors.js
import { createSelector } from '@reduxjs/toolkit';// 基础选择器
const selectTodos = (state) => state.todos.items;
const selectFilter = (state) => state.todos.filter;// 记忆化选择器
export const selectFilteredTodos = createSelector([selectTodos, selectFilter],(todos, filter) => {switch (filter) {case 'ACTIVE':return todos.filter(todo => !todo.completed);case 'COMPLETED':return todos.filter(todo => todo.completed);default:return todos;}}
);export const selectTodoStats = createSelector([selectTodos],(todos) => ({total: todos.length,completed: todos.filter(todo => todo.completed).length,active: todos.filter(todo => !todo.completed).length})
);// 参数化选择器
export const selectTodoById = createSelector([selectTodos, (state, todoId) => todoId],(todos, todoId) => todos.find(todo => todo.id === todoId)
);
3. 错误处理
// features/apiSlice.js
import { createSlice, createAsyncThunk } from '@reduxjs/toolkit';export const fetchData = createAsyncThunk('api/fetchData',async (params, { rejectWithValue, getState, signal }) => {try {const response = await fetch('/api/data', {signal, // 支持取消请求headers: {'Authorization': `Bearer ${getState().auth.token}`}});if (!response.ok) {const errorData = await response.json();throw new Error(errorData.message || 'Request failed');}return await response.json();} catch (error) {if (error.name === 'AbortError') {return rejectWithValue('Request was cancelled');}return rejectWithValue(error.message);}}
);const apiSlice = createSlice({name: 'api',initialState: {data: null,loading: false,error: null,retryCount: 0},reducers: {clearError: (state) => {state.error = null;state.retryCount = 0;},retry: (state) => {state.retryCount += 1;}},extraReducers: (builder) => {builder.addCase(fetchData.pending, (state) => {state.loading = true;state.error = null;}).addCase(fetchData.fulfilled, (state, action) => {state.loading = false;state.data = action.payload;state.retryCount = 0;}).addCase(fetchData.rejected, (state, action) => {state.loading = false;state.error = action.payload;// 自动重试逻辑if (state.retryCount < 3 && action.payload !== 'Request was cancelled') {state.retryCount += 1;// 可以在这里触发重试}});}
});
4. 性能优化
// 使用 React.memo 和 useCallback
import React, { memo, useCallback } from 'react';
import { useSelector, useDispatch } from 'react-redux';const TodoItem = memo(({ todoId }) => {const dispatch = useDispatch();// 只选择需要的数据const todo = useSelector(state => state.todos.items.find(item => item.id === todoId));// 使用 useCallback 避免不必要的重新渲染const handleToggle = useCallback(() => {dispatch(toggleTodo(todoId));}, [dispatch, todoId]);const handleDelete = useCallback(() => {dispatch(deleteTodo(todoId));}, [dispatch, todoId]);if (!todo) return null;return (<li className={todo.completed ? 'completed' : ''}><inputtype="checkbox"checked={todo.completed}onChange={handleToggle}/><span>{todo.text}</span><button onClick={handleDelete}>删除</button></li>);
});export default TodoItem;
5. 测试
// __tests__/todoSlice.test.js
import todoReducer, { addTodo, toggleTodo, deleteTodo } from '../features/todoSlice';describe('todoSlice', () => {const initialState = {items: [],filter: 'ALL',loading: false,error: null};it('should handle initial state', () => {expect(todoReducer(undefined, { type: 'unknown' })).toEqual(initialState);});it('should handle addTodo', () => {const actual = todoReducer(initialState, addTodo('Learn Redux'));expect(actual.items).toHaveLength(1);expect(actual.items[0].text).toEqual('Learn Redux');expect(actual.items[0].completed).toBe(false);});it('should handle toggleTodo', () => {const previousState = {...initialState,items: [{ id: 1, text: 'Learn Redux', completed: false }]};const actual = todoReducer(previousState, toggleTodo(1));expect(actual.items[0].completed).toBe(true);});it('should handle deleteTodo', () => {const previousState = {...initialState,items: [{ id: 1, text: 'Learn Redux', completed: false },{ id: 2, text: 'Build App', completed: true }]};const actual = todoReducer(previousState, deleteTodo(1));expect(actual.items).toHaveLength(1);expect(actual.items[0].id).toBe(2);});
});
// __tests__/selectors.test.js
import { selectFilteredTodos, selectTodoStats } from '../selectors/todoSelectors';describe('todo selectors', () => {const mockState = {todos: {items: [{ id: 1, text: 'Learn Redux', completed: false },{ id: 2, text: 'Build App', completed: true },{ id: 3, text: 'Test App', completed: false }],filter: 'ALL'}};it('should select all todos when filter is ALL', () => {const result = selectFilteredTodos(mockState);expect(result).toHaveLength(3);});it('should select active todos when filter is ACTIVE', () => {const state = {...mockState,todos: { ...mockState.todos, filter: 'ACTIVE' }};const result = selectFilteredTodos(state);expect(result).toHaveLength(2);expect(result.every(todo => !todo.completed)).toBe(true);});it('should calculate todo stats correctly', () => {const result = selectTodoStats(mockState);expect(result).toEqual({total: 3,completed: 1,active: 2});});
});
这份 Redux 使用指南涵盖了从基础概念到高级用法的所有重要内容。通过这些示例和最佳实践,你应该能够在项目中有效地使用 Redux 来管理应用状态。记住,Redux Toolkit 是现在推荐的方式,它简化了很多样板代码并提供了更好的开发体验。