# Redux完全指南
Redux是JavaScript应用的可预测状态容器,尤其适用于React应用。它帮助你编写行为一致、易于测试的应用,并提供出色的开发体验。本文将深入介绍Redux的核心概念、工作原理和最佳实践。
# 为什么需要Redux
在大型React应用中,组件状态管理变得复杂时,以下问题开始显现:
- 状态分散 - 状态分布在多个组件中,难以追踪和管理
- 状态共享困难 - 需要通过props多层传递或使用Context API
- 状态变化不可预测 - 多个组件可能以不同方式修改同一状态
- 调试困难 - 无法轻松回溯状态变化
Redux通过集中管理状态解决了这些问题。
# Redux核心概念
Redux架构基于三个基本原则:
- 单一数据源 - 整个应用的状态存储在单个store的对象树中
- 状态只读 - 唯一改变状态的方法是触发action
- 使用纯函数修改 - 使用reducer纯函数描述状态变化
# 1. Store
Store是Redux的核心,它保存整个应用的状态树:
import { createStore } from 'redux'
import rootReducer from './reducers'
const store = createStore(rootReducer)
2
3
4
# 2. Actions
Actions是描述发生了什么的简单对象,它们是改变状态的唯一方式:
// Action类型常量
export const ADD_TODO = 'ADD_TODO'
export const TOGGLE_TODO = 'TOGGLE_TODO'
// Action创建函数
export function addTodo(text) {
return {
type: ADD_TODO,
payload: {
id: Date.now(),
text,
completed: false
}
}
}
export function toggleTodo(id) {
return {
type: TOGGLE_TODO,
payload: { id }
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# 3. Reducers
Reducers指定状态如何响应actions变化。它是一个纯函数,接收先前的状态和action,返回新状态:
import { ADD_TODO, TOGGLE_TODO } from './actions'
const initialState = {
todos: []
}
function 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
)
}
default:
return state
}
}
export default todoReducer
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
对于复杂应用,可以使用combineReducers合并多个reducer:
import { combineReducers } from 'redux'
import todos from './todosReducer'
import auth from './authReducer'
const rootReducer = combineReducers({
todos,
auth
})
export default rootReducer
2
3
4
5
6
7
8
9
10
# Redux数据流
Redux遵循严格的单向数据流模式:
- 调用
store.dispatch(action) - Redux store调用reducer函数
- 根reducer将多个子reducer的结果合并成单个状态树
- Redux store保存reducer返回的完整状态树
- UI组件根据新状态更新
# 在React中使用Redux
# 设置React Redux
使用react-redux库将Redux与React集成:
npm install redux react-redux
# 或者
yarn add redux react-redux
2
3
# 连接组件
使用Provider和connect连接组件:
// 入口文件
import React from 'react'
import { render } from 'react-dom'
import { Provider } from 'react-redux'
import { createStore } from 'redux'
import rootReducer from './reducers'
import App from './components/App'
const store = createStore(rootReducer)
render(
<Provider store={store}>
<App />
</Provider>,
document.getElementById('root')
)
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
使用connect连接组件:
// TodoList.js
import React from 'react'
import { connect } from 'react-redux'
import { addTodo, toggleTodo } from '../actions'
function TodoList({ todos, addTodo, toggleTodo }) {
const [text, setText] = React.useState('')
const handleSubmit = e => {
e.preventDefault()
if (!text.trim()) return
addTodo(text)
setText('')
}
return (
<div>
<form onSubmit={handleSubmit}>
<input value={text} onChange={e => setText(e.target.value)} />
<button type="submit">Add Todo</button>
</form>
<ul>
{todos.map(todo => (
<li
key={todo.id}
onClick={() => toggleTodo(todo.id)}
style={{ textDecoration: todo.completed ? 'line-through' : 'none' }}
>
{todo.text}
</li>
))}
</ul>
</div>
)
}
const mapStateToProps = state => ({
todos: state.todos
})
const mapDispatchToProps = {
addTodo,
toggleTodo
}
export default connect(mapStateToProps, mapDispatchToProps)(TodoList)
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
# 使用React Redux Hooks
React Redux 7.1引入了Hooks API,提供更简洁的方式连接组件:
import React, { useState } from 'react'
import { useSelector, useDispatch } from 'react-redux'
import { addTodo, toggleTodo } from '../actions'
function TodoList() {
const [text, setText] = useState('')
const todos = useSelector(state => state.todos)
const dispatch = useDispatch()
const handleSubmit = e => {
e.preventDefault()
if (!text.trim()) return
dispatch(addTodo(text))
setText('')
}
return (
<div>
<form onSubmit={handleSubmit}>
<input value={text} onChange={e => setText(e.target.value)} />
<button type="submit">Add Todo</button>
</form>
<ul>
{todos.map(todo => (
<li
key={todo.id}
onClick={() => dispatch(toggleTodo(todo.id))}
style={{ textDecoration: todo.completed ? 'line-through' : 'none' }}
>
{todo.text}
</li>
))}
</ul>
</div>
)
}
export default TodoList
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
# 处理异步操作
Redux本身只处理同步数据流。处理异步API调用等副作用,需要中间件。
# Redux Thunk
redux-thunk是最简单的异步中间件:
npm install redux-thunk
配置:
import { createStore, applyMiddleware } from 'redux'
import thunk from 'redux-thunk'
import rootReducer from './reducers'
const store = createStore(
rootReducer,
applyMiddleware(thunk)
)
2
3
4
5
6
7
8
使用thunk创建异步action:
// 异步action创建器
export function fetchTodos() {
return async function(dispatch) {
dispatch({ type: 'FETCH_TODOS_REQUEST' })
try {
const response = await fetch('/api/todos')
const data = await response.json()
dispatch({
type: 'FETCH_TODOS_SUCCESS',
payload: data
})
} catch (error) {
dispatch({
type: 'FETCH_TODOS_FAILURE',
error: error.message
})
}
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# Redux Saga
对于复杂的异步流程,redux-saga提供更强大的方案:
npm install redux-saga
配置:
import { createStore, applyMiddleware } from 'redux'
import createSagaMiddleware from 'redux-saga'
import rootReducer from './reducers'
import rootSaga from './sagas'
const sagaMiddleware = createSagaMiddleware()
const store = createStore(
rootReducer,
applyMiddleware(sagaMiddleware)
)
sagaMiddleware.run(rootSaga)
2
3
4
5
6
7
8
9
10
11
12
13
编写saga:
import { call, put, takeEvery } from 'redux-saga/effects'
// Worker Saga
function* fetchTodos() {
try {
yield put({ type: 'FETCH_TODOS_REQUEST' })
const data = yield call(fetch, '/api/todos')
const todos = yield data.json()
yield put({ type: 'FETCH_TODOS_SUCCESS', payload: todos })
} catch (error) {
yield put({ type: 'FETCH_TODOS_FAILURE', error: error.message })
}
}
// Watcher Saga
function* todoSaga() {
yield takeEvery('FETCH_TODOS', fetchTodos)
}
export default todoSaga
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# Redux Toolkit
Redux Toolkit是Redux官方推荐的工具集,简化了Redux使用:
npm install @reduxjs/toolkit
使用Redux Toolkit创建slice:
import { createSlice, createAsyncThunk } from '@reduxjs/toolkit'
// 创建异步thunk
export const fetchTodos = createAsyncThunk(
'todos/fetchTodos',
async () => {
const response = await fetch('/api/todos')
return response.json()
}
)
const todosSlice = createSlice({
name: 'todos',
initialState: {
items: [],
status: 'idle',
error: null
},
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
}
}
},
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 } = todosSlice.actions
export default todosSlice.reducer
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
配置store:
import { configureStore } from '@reduxjs/toolkit'
import todosReducer from './todosSlice'
import authReducer from './authSlice'
const store = configureStore({
reducer: {
todos: todosReducer,
auth: authReducer
}
})
export default store
2
3
4
5
6
7
8
9
10
11
12
# 最佳实践
# 1. 状态规范化
对于嵌套或关联数据,规范化状态结构:
// 不好的例子 - 嵌套结构
const state = {
authors: [
{
id: 1,
name: '张三',
books: [
{ id: 1, title: 'JavaScript高级程序设计' },
{ id: 2, title: 'React实战' }
]
}
]
}
// 好的例子 - 规范化结构
const state = {
authors: {
byId: {
1: { id: 1, name: '张三', bookIds: [1, 2] }
},
allIds: [1]
},
books: {
byId: {
1: { id: 1, title: 'JavaScript高级程序设计', authorId: 1 },
2: { id: 2, title: 'React实战', authorId: 1 }
},
allIds: [1, 2]
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
# 2. 选择器函数
使用选择器(selectors)获取状态,提高重用性并封装状态结构:
// selectors.js
export const getTodos = state => state.todos.items
export const getTodoById = (state, id) =>
state.todos.items.find(todo => todo.id === id)
export const getCompletedTodos = state =>
state.todos.items.filter(todo => todo.completed)
2
3
4
5
6
使用选择器:
import { useSelector } from 'react-redux'
import { getTodos, getCompletedTodos } from '../selectors'
function TodoStats() {
const todos = useSelector(getTodos)
const completedTodos = useSelector(getCompletedTodos)
return (
<div>
<p>总任务数: {todos.length}</p>
<p>已完成: {completedTodos.length}</p>
<p>完成率: {Math.round((completedTodos.length / todos.length) * 100)}%</p>
</div>
)
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 3. 中间件优化
对于日志、崩溃报告等需求,使用中间件而非修改reducer:
import { createStore, applyMiddleware } from 'redux'
import logger from 'redux-logger'
import rootReducer from './reducers'
const crashReporter = store => next => action => {
try {
return next(action)
} catch (err) {
console.error('Caught an exception!', err)
// 发送到错误跟踪服务
Sentry.captureException(err, {
extra: {
action,
state: store.getState()
}
})
throw err
}
}
const store = createStore(
rootReducer,
applyMiddleware(logger, crashReporter)
)
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
# 实际项目示例
以下是一个电子商务应用的Redux结构示例:
src/
├── store/
│ ├── index.js # Store配置
│ ├── rootReducer.js # 根Reducer
│ ├── auth/ # 认证模块
│ │ ├── slice.js # Redux Toolkit slice
│ │ ├── selectors.js # 选择器
│ │ └── thunks.js # 异步thunks
│ ├── products/ # 产品模块
│ │ ├── slice.js
│ │ ├── selectors.js
│ │ └── thunks.js
│ └── cart/ # 购物车模块
│ ├── slice.js
│ ├── selectors.js
│ └── thunks.js
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
在我在字节跳动的工作中,我们使用Redux管理大型数据分析平台的状态。通过遵循模块化结构和严格的数据流原则,即使在10+开发人员同时工作的情况下,我们也能保持应用的可维护性和可预测性。
# 迁移到Redux Toolkit
如果你有现有的Redux代码库,可以逐步迁移到Redux Toolkit:
// 旧代码
const ADD_TODO = 'ADD_TODO'
const TOGGLE_TODO = 'TOGGLE_TODO'
// Action创建器
export const addTodo = text => ({
type: ADD_TODO,
payload: { text }
})
// Reducer
const initialState = { todos: [] }
export default function todosReducer(state = initialState, action) {
switch (action.type) {
case ADD_TODO:
return {
...state,
todos: [...state.todos, { id: Date.now(), text: action.payload.text, completed: false }]
}
// ...其他case
default:
return state
}
}
// 使用Redux Toolkit重写
import { createSlice } from '@reduxjs/toolkit'
const todosSlice = createSlice({
name: 'todos',
initialState: { todos: [] },
reducers: {
addTodo: (state, action) => {
state.todos.push({
id: Date.now(),
text: action.payload,
completed: false
})
},
toggleTodo: (state, action) => {
const todo = state.todos.find(todo => todo.id === action.payload)
if (todo) {
todo.completed = !todo.completed
}
}
}
})
export const { addTodo, toggleTodo } = todosSlice.actions
export default todosSlice.reducer
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
# Redux vs. Context API
React的Context API提供了共享状态的原生方法,但与Redux相比有不同权衡:
| 特性 | Redux | Context API |
|---|---|---|
| 状态管理 | 集中式 | 分散式 |
| 调试工具 | 强大 | 有限 |
| 中间件支持 | 丰富 | 需自行实现 |
| 性能优化 | 细粒度订阅 | 较广泛的重渲染 |
| 学习曲线 | 陡峭 | 平缓 |
| 适用场景 | 大型应用 | 小到中型应用 |
# 总结
Redux提供了一种可预测的状态管理方式,尤其适合复杂的React应用。其核心概念——单一数据源、只读状态和纯函数reducer——共同构成了一个强大而灵活的状态管理解决方案。
虽然学习曲线较陡,但Redux的长期收益显著。随着Redux Toolkit的发展,使用Redux变得更加简单和高效。
在选择状态管理解决方案时,请考虑项目规模、团队熟悉度和应用需求。对于大型、复杂的应用,Redux仍然是一个值得投资的解决方案。