Redux 教程
1. 项目概述
Redux 是 JavaScript 应用的可预测状态容器,它可以帮助你编写行为一致、可预测且易于测试的应用。Redux 主要用于管理应用的状态,特别是在大型应用中,它可以使状态管理变得更加可预测和可维护。
1.1 主要特性
- 单一数据源:整个应用的状态存储在一个单一的 store 中
- 状态是只读的:唯一改变状态的方法是触发 action
- 使用纯函数来执行修改:通过 reducer 纯函数来处理状态更新
- 可预测性:相同的输入总是产生相同的输出
- 可测试性:纯函数易于测试
- 中间件支持:通过中间件处理异步操作和副作用
- 时间旅行调试:可以回溯和重放状态变化
- 生态系统丰富:有大量的插件和工具
1.2 适用场景
- 大型前端应用
- 状态逻辑复杂的应用
- 需要可预测状态管理的应用
- 需要时间旅行调试的应用
- 多组件共享状态的应用
- 服务器端渲染的应用
2. 安装与设置
2.1 环境要求
- Node.js 14.0 或更高版本
- npm 或 yarn 包管理器
- 基本的 JavaScript 知识
2.2 安装步骤
方法一:在现有项目中安装
# 安装 Redux 核心库
npm install redux
# 如果你使用 React,还需要安装 React-Redux
npm install react-redux
# 如果你需要处理异步操作,还需要安装 Redux-Thunk
npm install redux-thunk
# 如果你需要使用 Redux DevTools,还需要安装
npm install --save-dev redux-devtools-extension方法二:使用 Create React App
# 创建带有 Redux 的 React 应用
npx create-react-app my-redux-app --template redux
# 进入项目目录
cd my-redux-app
# 启动开发服务器
npm start2.3 基本项目结构
my-redux-app/
├── src/
│ ├── app/
│ │ └── store.js # Redux store 配置
│ ├── features/
│ │ └── counter/
│ │ ├── Counter.js # 组件
│ │ └── counterSlice.js # Redux slice
│ ├── index.js # 应用入口
│ └── App.js # 根组件
├── package.json # 项目配置
└── package-lock.json # 依赖锁定3. 核心概念
3.1 Action
Action 是一个带有 type 字段的普通 JavaScript 对象,它描述了发生了什么。
// 基本 action
const increment = {
type: 'counter/increment'
};
// 带 payload 的 action
const addTodo = {
type: 'todos/addTodo',
payload: 'Learn Redux'
};3.2 Reducer
Reducer 是一个纯函数,它接收当前的状态和一个 action,然后返回一个新的状态。
// 基本 reducer
function counterReducer(state = 0, action) {
switch (action.type) {
case 'counter/increment':
return state + 1;
case 'counter/decrement':
return state - 1;
default:
return state;
}
}3.3 Store
Store 是保存应用状态的对象,它提供了几个方法来访问和修改状态。
// 创建 store
import { createStore } from 'redux';
import counterReducer from './reducers/counterReducer';
const store = createStore(counterReducer);
// 获取状态
const state = store.getState();
// 分发 action
store.dispatch({ type: 'counter/increment' });
// 订阅状态变化
store.subscribe(() => {
console.log('State changed:', store.getState());
});3.4 Dispatch
Dispatch 是 store 的一个方法,用于分发 action 来修改状态。
store.dispatch({ type: 'counter/increment' });3.5 Subscribe
Subscribe 是 store 的一个方法,用于订阅状态变化,当状态发生变化时执行回调函数。
const unsubscribe = store.subscribe(() => {
console.log('State changed:', store.getState());
});
// 取消订阅
unsubscribe();3.6 Middleware
Middleware 是 Redux 的扩展点,用于处理异步操作、日志记录等副作用。
// 日志中间件
const loggerMiddleware = store => next => action => {
console.log('Dispatching:', action);
const result = next(action);
console.log('Next state:', store.getState());
return result;
};4. 基本使用
4.1 创建 Redux Store
// src/app/store.js
import { createStore } from 'redux';
import rootReducer from '../reducers';
const store = createStore(rootReducer);
export default store;4.2 创建 Reducer
// src/reducers/index.js
import { combineReducers } from 'redux';
import counterReducer from './counterReducer';
import todosReducer from './todosReducer';
const rootReducer = combineReducers({
counter: counterReducer,
todos: todosReducer
});
export default rootReducer;// src/reducers/counterReducer.js
const initialState = 0;
function counterReducer(state = initialState, action) {
switch (action.type) {
case 'counter/increment':
return state + 1;
case 'counter/decrement':
return state - 1;
case 'counter/reset':
return 0;
case 'counter/incrementByAmount':
return state + action.payload;
default:
return state;
}
}
export default counterReducer;4.3 创建 Action Creators
// src/actions/counterActions.js
export const increment = () => ({
type: 'counter/increment'
});
export const decrement = () => ({
type: 'counter/decrement'
});
export const reset = () => ({
type: 'counter/reset'
});
export const incrementByAmount = (amount) => ({
type: 'counter/incrementByAmount',
payload: amount
});4.4 在 React 中使用 Redux
使用 connect 函数
// src/components/Counter.js
import React from 'react';
import { connect } from 'react-redux';
import { increment, decrement, reset, incrementByAmount } from '../actions/counterActions';
function Counter({ count, increment, decrement, reset, incrementByAmount }) {
return (
<div>
<h1>Counter: {count}</h1>
<button onClick={increment}>Increment</button>
<button onClick={decrement}>Decrement</button>
<button onClick={reset}>Reset</button>
<button onClick={() => incrementByAmount(5)}>Increment by 5</button>
</div>
);
}
const mapStateToProps = (state) => ({
count: state.counter
});
const mapDispatchToProps = {
increment,
decrement,
reset,
incrementByAmount
};
export default connect(mapStateToProps, mapDispatchToProps)(Counter);使用 Hooks
// src/components/Counter.js
import React from 'react';
import { useSelector, useDispatch } from 'react-redux';
import { increment, decrement, reset, incrementByAmount } from '../actions/counterActions';
function Counter() {
const count = useSelector(state => state.counter);
const dispatch = useDispatch();
return (
<div>
<h1>Counter: {count}</h1>
<button onClick={() => dispatch(increment())}>Increment</button>
<button onClick={() => dispatch(decrement())}>Decrement</button>
<button onClick={() => dispatch(reset())}>Reset</button>
<button onClick={() => dispatch(incrementByAmount(5))}>Increment by 5</button>
</div>
);
}
export default Counter;4.5 提供 Store
// src/index.js
import React from 'react';
import ReactDOM from 'react-dom';
import { Provider } from 'react-redux';
import store from './app/store';
import App from './App';
ReactDOM.render(
<React.StrictMode>
<Provider store={store}>
<App />
</Provider>
</React.StrictMode>,
document.getElementById('root')
);5. 高级功能
5.1 异步操作
使用 Redux-Thunk
// src/actions/todoActions.js
import axios from 'axios';
export const fetchTodos = () => async (dispatch) => {
dispatch({ type: 'todos/fetchTodosStart' });
try {
const response = await axios.get('https://jsonplaceholder.typicode.com/todos');
dispatch({
type: 'todos/fetchTodosSuccess',
payload: response.data
});
} catch (error) {
dispatch({
type: 'todos/fetchTodosError',
payload: error.message
});
}
};配置 Redux-Thunk
// src/app/store.js
import { createStore, applyMiddleware } from 'redux';
import thunk from 'redux-thunk';
import rootReducer from '../reducers';
const store = createStore(
rootReducer,
applyMiddleware(thunk)
);
export default store;5.2 Redux Toolkit
Redux Toolkit 是官方推荐的 Redux 开发工具集,它简化了 Redux 的使用。
安装 Redux Toolkit
npm install @reduxjs/toolkit创建 Slice
// src/features/counter/counterSlice.js
import { createSlice } from '@reduxjs/toolkit';
const initialState = {
value: 0,
status: 'idle'
};
export const counterSlice = createSlice({
name: 'counter',
initialState,
reducers: {
increment: (state) => {
state.value += 1;
},
decrement: (state) => {
state.value -= 1;
},
incrementByAmount: (state, action) => {
state.value += action.payload;
}
}
});
export const { increment, decrement, incrementByAmount } = counterSlice.actions;
export default counterSlice.reducer;配置 Store
// src/app/store.js
import { configureStore } from '@reduxjs/toolkit';
import counterReducer from '../features/counter/counterSlice';
import todosReducer from '../features/todos/todosSlice';
export const store = configureStore({
reducer: {
counter: counterReducer,
todos: todosReducer
}
});5.3 选择器
选择器是用于从 Redux 状态中提取数据的函数,它可以帮助你避免重复的状态选择逻辑。
// src/features/todos/todosSlice.js
import { createSlice, createSelector } from '@reduxjs/toolkit';
// ... slice 代码 ...
// 选择器
export const selectAllTodos = (state) => state.todos.items;
export const selectCompletedTodos = createSelector(
[selectAllTodos],
(todos) => todos.filter(todo => todo.completed)
);
export const selectTodoById = (state, todoId) =>
state.todos.items.find(todo => todo.id === todoId);使用选择器
// src/components/TodoList.js
import React from 'react';
import { useSelector } from 'react-redux';
import { selectAllTodos, selectCompletedTodos } from '../features/todos/todosSlice';
function TodoList() {
const todos = useSelector(selectAllTodos);
const completedTodos = useSelector(selectCompletedTodos);
return (
<div>
<h2>All Todos ({todos.length})</h2>
<ul>
{todos.map(todo => (
<li key={todo.id}>{todo.text}</li>
))}
</ul>
<h2>Completed Todos ({completedTodos.length})</h2>
<ul>
{completedTodos.map(todo => (
<li key={todo.id}>{todo.text}</li>
))}
</ul>
</div>
);
}
export default TodoList;5.4 中间件
创建自定义中间件
// src/middleware/logger.js
export const loggerMiddleware = store => next => action => {
console.log('Dispatching:', action);
const result = next(action);
console.log('Next state:', store.getState());
return result;
};使用中间件
// src/app/store.js
import { configureStore } from '@reduxjs/toolkit';
import counterReducer from '../features/counter/counterSlice';
import { loggerMiddleware } from '../middleware/logger';
export const store = configureStore({
reducer: {
counter: counterReducer
},
middleware: (getDefaultMiddleware) =>
getDefaultMiddleware().concat(loggerMiddleware)
});5.5 持久化
安装 Redux Persist
npm install redux-persist配置持久化
// src/app/store.js
import { configureStore, getDefaultMiddleware } from '@reduxjs/toolkit';
import { persistStore, persistReducer } from 'redux-persist';
import storage from 'redux-persist/lib/storage';
import counterReducer from '../features/counter/counterSlice';
const persistConfig = {
key: 'root',
storage,
};
const persistedReducer = persistReducer(persistConfig, counterReducer);
export const store = configureStore({
reducer: {
counter: persistedReducer
},
middleware: getDefaultMiddleware({
serializableCheck: false
})
});
export const persistor = persistStore(store);使用 PersistGate
// src/index.js
import React from 'react';
import ReactDOM from 'react-dom';
import { Provider } from 'react-redux';
import { PersistGate } from 'redux-persist/integration/react';
import { store, persistor } from './app/store';
import App from './App';
ReactDOM.render(
<React.StrictMode>
<Provider store={store}>
<PersistGate loading={null} persistor={persistor}>
<App />
</PersistGate>
</Provider>
</React.StrictMode>,
document.getElementById('root')
);6. 实用案例
6.1 待办事项应用
创建 Todo Slice
// src/features/todos/todosSlice.js
import { createSlice, createAsyncThunk } from '@reduxjs/toolkit';
import axios from 'axios';
export const fetchTodos = createAsyncThunk(
'todos/fetchTodos',
async () => {
const response = await axios.get('https://jsonplaceholder.typicode.com/todos');
return response.data;
}
);
const initialState = {
items: [],
status: 'idle',
error: null
};
export const todosSlice = createSlice({
name: 'todos',
initialState,
reducers: {
addTodo: (state, action) => {
state.items.push({
id: Date.now(),
text: action.payload,
completed: false
});
},
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);
}
},
extraReducers: (builder) => {
builder
.addCase(fetchTodos.pending, (state) => {
state.status = 'loading';
})
.addCase(fetchTodos.fulfilled, (state, action) => {
state.status = 'succeeded';
state.items = action.payload;
})
.addCase(fetchTodos.rejected, (state, action) => {
state.status = 'failed';
state.error = action.error.message;
});
}
});
export const { addTodo, toggleTodo, deleteTodo } = todosSlice.actions;
export default todosSlice.reducer;创建 Todo 组件
// src/components/TodoList.js
import React, { useEffect } from 'react';
import { useSelector, useDispatch } from 'react-redux';
import {
fetchTodos,
addTodo,
toggleTodo,
deleteTodo
} from '../features/todos/todosSlice';
function TodoList() {
const dispatch = useDispatch();
const { items: todos, status, error } = useSelector(state => state.todos);
const [inputText, setInputText] = React.useState('');
useEffect(() => {
if (status === 'idle') {
dispatch(fetchTodos());
}
}, [status, dispatch]);
const handleAddTodo = () => {
if (inputText.trim()) {
dispatch(addTodo(inputText));
setInputText('');
}
};
return (
<div>
<h1>Todo List</h1>
<div>
<input
type="text"
value={inputText}
onChange={(e) => setInputText(e.target.value)}
placeholder="Add a todo"
/>
<button onClick={handleAddTodo}>Add</button>
</div>
{status === 'loading' && <p>Loading...</p>}
{status === 'failed' && <p>Error: {error}</p>}
<ul>
{todos.map(todo => (
<li key={todo.id}>
<input
type="checkbox"
checked={todo.completed}
onChange={() => dispatch(toggleTodo(todo.id))}
/>
<span style={{
textDecoration: todo.completed ? 'line-through' : 'none'
}}>
{todo.text}
</span>
<button onClick={() => dispatch(deleteTodo(todo.id))}>Delete</button>
</li>
))}
</ul>
</div>
);
}
export default TodoList;6.2 计数器应用
创建 Counter Slice
// src/features/counter/counterSlice.js
import { createSlice } from '@reduxjs/toolkit';
const initialState = {
value: 0
};
export const counterSlice = createSlice({
name: 'counter',
initialState,
reducers: {
increment: (state) => {
state.value += 1;
},
decrement: (state) => {
state.value -= 1;
},
incrementByAmount: (state, action) => {
state.value += action.payload;
},
reset: (state) => {
state.value = 0;
}
}
});
export const { increment, decrement, incrementByAmount, reset } = counterSlice.actions;
export default counterSlice.reducer;创建 Counter 组件
// src/components/Counter.js
import React from 'react';
import { useSelector, useDispatch } from 'react-redux';
import { increment, decrement, incrementByAmount, reset } from '../features/counter/counterSlice';
function Counter() {
const count = useSelector(state => state.counter.value);
const dispatch = useDispatch();
return (
<div>
<h1>Counter: {count}</h1>
<button onClick={() => dispatch(increment())}>Increment</button>
<button onClick={() => dispatch(decrement())}>Decrement</button>
<button onClick={() => dispatch(reset())}>Reset</button>
<button onClick={() => dispatch(incrementByAmount(10))}>Increment by 10</button>
</div>
);
}
export default Counter;7. 性能优化
7.1 避免不必要的渲染
- 使用 useSelector 的正确方式:使用选择器函数而不是对象解构
- 使用 createSelector:创建记忆化的选择器
- 使用 React.memo:包装组件以避免不必要的渲染
- 使用 batch:批量处理多个 dispatch
7.2 状态规范化
- 规范化状态结构:使用 ID 作为键,而不是数组
- 避免深层嵌套:保持状态结构扁平化
- 使用实体适配器:使用 Redux Toolkit 的 createEntityAdapter
7.3 异步操作优化
- 使用 createAsyncThunk:处理异步操作
- 避免重复请求:使用状态来跟踪请求状态
- 使用缓存:缓存频繁使用的数据
7.4 代码分割
- 使用动态导入:按需加载 Redux 模块
- 使用 Redux Injectors:动态注入 reducers
8. 最佳实践
8.1 项目结构
- 按功能组织:将相关的 reducer、action 和组件放在一起
- 使用 ducks 模式:将 action、reducer 和选择器放在一个文件中
- 使用 Redux Toolkit:使用官方推荐的工具集
8.2 Action 设计
- 使用领域特定的 action 类型:如
counter/increment - 使用 payload 属性:传递额外的数据
- 使用 action creators:创建 action 的函数
- 保持 action 简洁:每个 action 只做一件事
8.3 Reducer 设计
- 使用纯函数:不修改输入状态,返回新状态
- 使用默认参数:设置初始状态
- 处理所有 action:使用 default 分支
- 使用 Immer:通过 Redux Toolkit 使用 Immer 简化状态更新
8.4 中间件使用
- 使用 Redux Thunk:处理异步操作
- 使用 Redux Saga:处理复杂的异步流程
- 使用 Redux Observable:使用 RxJS 处理异步操作
- 使用日志中间件:开发时使用
8.5 测试
- 测试 action creators:测试返回正确的 action
- 测试 reducers:测试状态更新
- 测试选择器:测试数据提取
- 测试异步操作:测试异步流程
- 测试组件:测试组件与 Redux 的交互
9. 常见问题与解决方案
9.1 状态更新不生效
问题:dispatch action 后状态没有更新
解决方案:
- 检查 reducer 是否返回新状态
- 检查 action type 是否正确
- 检查 reducer 是否处理了该 action
- 检查是否使用了纯函数
9.2 组件不重新渲染
问题:状态更新后组件没有重新渲染
解决方案:
- 检查 useSelector 是否正确使用
- 检查是否返回了新的引用
- 检查组件是否被 React.memo 包装
- 检查是否有不必要的依赖
9.3 异步操作错误
问题:异步操作失败或不执行
解决方案:
- 检查中间件是否正确配置
- 检查异步 action creator 是否正确
- 检查网络请求是否正确
- 检查错误处理是否完善
9.4 状态结构过于复杂
问题:状态结构深层嵌套,难以管理
解决方案:
- 规范化状态结构
- 使用实体适配器
- 分解 reducer
- 使用 combineReducers
9.5 Redux DevTools 不工作
问题:Redux DevTools 无法连接或显示状态
解决方案:
- 检查是否安装了 redux-devtools-extension
- 检查 store 配置是否正确
- 检查浏览器是否安装了 Redux DevTools 扩展
- 检查是否有语法错误
10. 参考资源
10.1 官方文档
10.2 学习资源
10.3 工具与库
- Redux Toolkit - 官方推荐的 Redux 开发工具集
- React-Redux - React 绑定
- Redux-Thunk - 处理异步操作
- Redux-Saga - 处理复杂的异步流程
- Redux-Observable - 使用 RxJS 处理异步操作
- Redux Persist - 持久化 Redux 状态
10.4 示例项目
11. 总结
Redux 是一个强大的状态管理库,它通过单一数据源、纯函数和可预测的状态更新,为 JavaScript 应用提供了一种一致的状态管理方案。通过本教程,你应该已经掌握了 Redux 的基本使用方法和高级功能,包括:
- 核心概念(action、reducer、store)
- 基本使用
- 异步操作
- Redux Toolkit
- 选择器
- 中间件
- 持久化
- 性能优化
- 最佳实践
Redux 的设计理念是 "可预测的状态管理",它通过严格的规则和模式,使得状态管理变得更加可预测和可维护。虽然 Redux 有一定的学习曲线,但是一旦掌握了它的核心概念和最佳实践,它将成为你开发大型应用的得力助手。
Redux Toolkit 的出现大大简化了 Redux 的使用,它提供了一套开箱即用的工具,使得 Redux 的开发变得更加高效和愉快。官方推荐使用 Redux Toolkit 来开发 Redux 应用。
Redux 适合从简单的个人项目到复杂的企业级应用的各种场景。它的生态系统非常丰富,有大量的插件和工具,可以满足各种需求。
随着前端技术的不断发展,Redux 也在不断演进,提供了更多的特性和优化。希望本教程对你的学习和开发有所帮助!