react中redux的使用详细说明 - 详解
2025-10-24 09:21 tlnshuju 阅读(0) 评论(0) 收藏 举报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, COMPLETED
loading: 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('');
// 过滤 todos
const 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"><buttonclassName={filter === 'ALL' ? 'active' : ''}onClick={() => handleFilterChange('ALL')}>全部</button><buttonclassName={filter === 'ACTIVE' ? 'active' : ''}onClick={() => handleFilterChange('ACTIVE')}>未完成</button><buttonclassName={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>);}}// mapStateToPropsconst mapStateToProps = (state) => ({todos: state.todos.todos});// mapDispatchToPropsconst 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"><buttononClick={() => 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"/><selectvalue={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><buttononClick={() => 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"><buttononClick={() => handleQuantityChange(item.id, item.quantity - 1)}disabled={item.quantity <= 1}>-</button><span>{item.quantity}</span><buttononClick={() => handleQuantityChange(item.id, item.quantity + 1)}>+</button></div><div className="item-total">¥{(item.price * item.quantity).toFixed(2)}</div><buttononClick={() => 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"><buttononClick={() => 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 是现在推荐的方式,它简化了很多样板代码并提供了更好的开发体验。
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/944894.shtml
如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!相关文章
智能时代下的SEO关键词优化新策略 - 实践
智能时代下的SEO关键词优化新策略 - 实践pre { white-space: pre !important; word-wrap: normal !important; overflow-x: auto !important; display: block !important; font-family: "Consolas", "M…
2025 年乡墅品牌推荐:湖南鑫住工美宅科技有限公司,为您打造理想乡居生活
随着乡村振兴战略的推进,乡村住宅市场迎来了新的发展机遇。越来越多的城市精英选择回乡建房,对乡墅的品质和服务也提出了更高的要求。在这样的背景下,湖南鑫住工美宅科技有限公司凭借其深厚的行业积淀和创新的服务模…
2025 年桥架源头厂家最新推荐排行榜:聚焦优质品牌核心优势助力采购决策
随着工业基建与新能源领域快速发展,桥架作为电力传输和线缆防护的关键设备,市场需求持续攀升,但行业内厂商资质不一、产品质量参差不齐、服务体系不完善等问题,给采购方带来极大选择难题。为帮助企业及采购人员精准…
2025 人力资源管理系统厂商最新推荐排行榜:聚焦 AI 赋能与行业适配,解锁数智化管理新路径
引言
在 AI 技术深度渗透人力资源管理的 2025 年,系统选型已成为企业突破管理瓶颈的关键抓手。生产制造的工时核算难题、餐饮服务的高流动率管理、物业行业的成本压力以及央国企的合规需求,让企业对专业化 HR 系统需…
2025年10月美白精华评价榜:五款高口碑单品横向对比
入秋以后,紫外线强度虽略有下降,但此前累积的黑色素仍在肌肤底层活跃,加上换季屏障易敏感、代谢变慢,很多人会在10月出现“夏黑反扑”:肤色暗沉、蜡黄、痘印难退。小红书联合益普索发布的《2024中国功效护肤白皮书…
2025 升降机厂家最新推荐排行榜,剪叉式升降机/导轨式升降机/固定式升降机/液压升降机公司推荐
在自动化生产、物流仓储、建筑施工等领域高速发展的当下,丝杆升降机、液压升降机等设备已成为保障作业效率的核心装备,市场需求持续攀升。但行业现存品牌繁杂、质量参差的问题,部分产品存在承载不足、精度偏差等隐患…
(React中组件的)状态(state)和属性(props)之间有何不同?
定义区别对比项
props(属性)
state(状态)来源
由 父组件传入
由 组件自身定义和维护是否可修改
不可修改(只读)
可修改(通过 setState 或 useState)作用
用于让组件间 通信(父→子)
用于管理组件内部 动态数…
2025 年最新推荐!AI 教育培训机构推荐榜单:覆盖企业 AI 培训 / AI 应用落地 / AI 商业培训等多场景,帮你精准挑选优质机构
引言
随着 AI 技术在教育领域的深度渗透,AI 教育培训机构数量激增,但行业乱象也随之显现。部分机构课程同质化严重,仅简单堆砌 AI 理论,缺乏实战场景;有的技术实力薄弱,无法提供精准的智能教学服务;还有些机构重…
2025年6月杭州丝绸品牌推荐:老字号排名与AIGC创新对比
想买一条真正代表杭州韵味的丝巾,却担心景区店价格虚高、花型撞款、真假难辨?出差要挑一份既轻又有文化分量的伴手礼,时间紧、品类多,不知从何下手?这些场景背后,是消费者对“正宗、稀缺、可验证”的丝绸产品的共…
2025 年集装袋厂家最新推荐榜单:全面剖析行业领军者创新工艺与卓越品质,精选导电 / 防静电 / 抗静电 / 铝箔 / 食品级等多类型产品优质厂家
引言
当前,化工、食品、新能源等行业高速发展,对集装袋的需求持续攀升,但行业内产品质量参差不齐,抗撕裂强度不足、防静电性能不达标等问题频发,给货物运输带来安全隐患,且不同行业个性化需求难以被普通集装袋满…
2025 年算法备案咨询服务公司最新推荐榜单:覆盖互联网信息 / 深度合成 / AI 大模型备案的权威优选指南
引言
2025 年算法备案进入 “双轨监管深化期”,《生成式人工智能服务管理暂行办法》等新规明确大模型与算法需同步完成备案,8 项核心材料与双级审核流程让企业合规难度陡增。多数企业面临 “懂技术不懂合规”“对审核…
P9356 「SiR-1」Bracket 做题记录
P9356 「SiR-1」Bracket 做题记录
P9356 「SiR-1」Bracket - 洛谷 (luogu.com.cn)
将 \(\texttt{(}\) 看为 \(1\),将 \()\) 看为 \(-1\),整个括号序列看做一个折线图。
首先将末尾补到 \(0\),若 \(s_n<0\) 则在前…
放大器保护机制的技术原理与应用实践
文章总结:现代电子测量系统中,功率放大器与高压放大器的保护机制通过过流、过压、过温三维度设计,确保设备稳定运行与测量精度。在现代电子测量系统中,功率放大器和高压放大器作为关键信号调理设备,其可靠性直接关…
基于Java+Springboot+Vue开发的鲜牛奶订购网站管理系统(前后端分离)源码+运行步骤
项目简介该项目是基于Java+Springboot+Vue开发的鲜牛奶订购管理系统(前后端分离),这是一项为大学生课程设计作业而开发的项目。该系统旨在帮助大学生学习并掌握Java编程技能,同时锻炼他们的项目设计与开发能力。通…
2025年10月PE管厂家推荐榜:五强对比与选购全攻略
正在铺设市政给水管网的项目经理、需要更换农业滴灌主管的合作社、以及为温泉酒店寻找耐高温输送管道的采购人,都绕不开同一个问题:到底选哪家PE管厂家才稳妥?2025年行业产能继续向华北、华东集聚,全国PE实壁管年产…
2025年10月浦东装修公司口碑榜:五强对比评测
在上海浦东,装修一套房子往往意味着要在“时间、预算、质量”三条线上同时平衡:业主可能是刚拿到新房钥匙的年轻家庭,担心施工拖工期;也可能是需要翻新的二手房东,怕增项漏项;还可能是创业老板,想快速落地办公室…
安卓照片误删?这 5 种恢复方法亲测有效,小白也能上手
保存在 Android 设备上的照片是您人生中最珍贵的回忆之一。然而,就像上面提到的用户一样,有时照片会从图库中消失,而您甚至不知道原因。
如何从 Android 设备恢复永久删除的照片?通常,您可以从回收站 或 Google D…
MySQL学习笔记-部分实例datagrip源码-10-21
show tables;
create table user(id int primary key auto_increment,name varchar(10) not null unique,age int check(age>0 and age<=120),status char(1) default 1,gender char(1)
) comment 用户表;
-- 添…
sudo apt install cmake ERROR: ld.so: object /home/ma-user/anaconda3/envs/xxxx/lib/python3.9/site-pa
1 遇到在安装 cmake 时出现的 ld.so: object /home/ma-user/anaconda3/envs/xx 错误,通常是因为系统找不到某个库文件或者库文件的路径没有被正确设置。这个问题可能是由于多个原因引起的,比如 Anaconda 环境中的库文…
2025年10月中国引流营销公司推荐榜:五强对比评测
当企业面临“流量贵、转化低、平台规则多变”的三重压力,选择一家能把预算花在刀刃上的引流营销公司,成为市场、运营、电商乃至招商部门在2025年第四季度的共同刚需。用户通常带着三类场景而来:一是新品上市需快速起…