React全家桶笔记(八):Redux与React-Redux状态管理
本篇系统讲解 Redux 的设计思想、工作流程,以及 react-redux 的使用方式。通过求和案例从零到一掌握 Redux 状态管理的完整链路。 📺 对应张天禹react全家桶视频:P97 - P114
一、Redux 简介(P97)
1.1 为什么需要 Redux?
随着应用变复杂,组件之间的状态共享变得困难:
- 兄弟组件通信需要状态提升到父组件
- 跨层级组件通信更加繁琐
- 多个组件依赖同一份数据时,状态管理混乱
Redux 是一个专门用于做状态管理的 JS 库(不是 React 插件库),可以用在任何 JS 项目中,但基本与 React 配合使用。
1.2 什么时候用 Redux?
- 某个组件的状态需要让其他组件随时拿到(共享)
- 一个组件需要改变另一个组件的状态(通信)
- 总体原则:能不用就不用,如果不用比较吃力才考虑使用
1.3 Redux 三大原则
1. 单一数据源(Single Source of Truth)
→ 整个应用的 state 存储在一棵对象树中,且只存在于唯一一个 store 中
2. State 是只读的(State is Read-only)
→ 唯一改变 state 的方法就是触发 action
3. 使用纯函数来执行修改(Changes are made with Pure Functions)
→ Reducer 必须是纯函数
二、Redux 工作流程(P98)
2.1 核心概念
Action(动作):
├── 一个普通 JS 对象
├── 必须有 type 属性(标识动作类型)
├── 可选 data 属性(携带数据)
└── 例:{ type: 'increment', data: 1 }
Reducer(处理器):
├── 一个纯函数
├── 接收两个参数:previousState(旧状态)和 action(动作)
├── 返回新的 state
└── 初始化时 previousState 是 undefined,需要设置默认值
Store(仓库):
├── 将 Action 和 Reducer 联系在一起的对象
├── 整个应用只有一个 Store
├── getState():获取 state
├── dispatch(action):分发 action,触发 reducer
└── subscribe(listener):注册监听,state 变化时自动调用
2.2 工作流程图
组件(Component)
│
│ 1. dispatch(action) → 分发动作
▼
Store ──────────────────→ Reducer
│ 传递:(previousState, action) │
│ │
│ ◄──── 返回:newState ────────────┘
│
│ 2. getState() → 获取新状态
▼
组件重新渲染
通俗理解:
- Store 是餐厅老板
- Action 是顾客的点单("我要加1")
- Reducer 是厨师(根据点单和现有食材,做出新菜)
- Component 是顾客(点单,然后等着上菜)
三、求和案例 — 纯 React 版(P99)
先用纯 React 实现,作为对比基准:
export default class Count extends Component {
state = { count: 0 }
increment = () => {
const { value } = this.selectNumber
this.setState({ count: this.state.count + value * 1 })
}
decrement = () => {
const { value } = this.selectNumber
this.setState({ count: this.state.count - value * 1 })
}
incrementIfOdd = () => {
const { value } = this.selectNumber
if (this.state.count % 2 !== 0) {
this.setState({ count: this.state.count + value * 1 })
}
}
incrementAsync = () => {
const { value } = this.selectNumber
setTimeout(() => {
this.setState({ count: this.state.count + value * 1 })
}, 500)
}
render() {
return (
<div>
<h1>当前求和为:{this.state.count}</h1>
<select ref={c => this.selectNumber = c}>
<option value="1">1</option>
<option value="2">2</option>
<option value="3">3</option>
</select>
<button onClick={this.increment}>+</button>
<button onClick={this.decrement}>-</button>
<button onClick={this.incrementIfOdd}>当前求和为奇数再加</button>
<button onClick={this.incrementAsync}>异步加</button>
</div>
)
}
}
四、求和案例 — Redux 精简版(P100)
4.1 安装
npm install redux
4.2 文件结构
src/
├── redux/
│ ├── store.js → 创建 store
│ └── count_reducer.js → count 的 reducer
└── components/
└── Count/index.jsx
4.3 创建 Reducer
// src/redux/count_reducer.js
// 初始化状态
const initState = 0
export default function countReducer(preState = initState, action) {
const { type, data } = action
switch (type) {
case 'increment':
return preState + data
case 'decrement':
return preState - data
default:
return preState // 初始化时走这里
}
}
4.4 创建 Store
// src/redux/store.js
import { createStore } from 'redux'
import countReducer from './count_reducer'
export default createStore(countReducer)
4.5 组件中使用
// src/components/Count/index.jsx
import store from '../../redux/store'
export default class Count extends Component {
componentDidMount() {
// 监听 store 中状态的变化,一旦变化就重新渲染
store.subscribe(() => {
this.setState({}) // 调用 setState 触发重新渲染
})
}
increment = () => {
const { value } = this.selectNumber
// 通知 store 执行加法
store.dispatch({ type: 'increment', data: value * 1 })
}
decrement = () => {
const { value } = this.selectNumber
store.dispatch({ type: 'decrement', data: value * 1 })
}
render() {
return (
<div>
{/* 从 store 中获取状态 */}
<h1>当前求和为:{store.getState()}</h1>
{/* ... 按钮等 */}
</div>
)
}
}
⚠️ 注意:Redux 只负责管理状态,不会自动触发页面更新。需要通过
store.subscribe()监听变化,手动触发组件重新渲染。
💡 更优雅的写法:在
index.js入口文件中统一监听:
store.subscribe(() => {
ReactDOM.render(<App/>, document.getElementById('root'))
})
这样所有组件都能自动响应 store 变化(不用担心性能,React 的 Diffing 算法会保证只更新变化的部分)。
五、求和案例 — Redux 完整版(P101)
5.1 新增 Action Creator
src/redux/
├── store.js
├── count_reducer.js
├── count_action.js → 新增:Action 创建函数
└── constant.js → 新增:常量模块
// src/redux/constant.js — 定义 action type 常量,防止拼写错误
export const INCREMENT = 'increment'
export const DECREMENT = 'decrement'
// src/redux/count_action.js — Action Creator
import { INCREMENT, DECREMENT } from './constant'
// 同步 action:返回一个 Object
export const createIncrementAction = data => ({ type: INCREMENT, data })
export const createDecrementAction = data => ({ type: DECREMENT, data })
// count_reducer.js 也使用常量
import { INCREMENT, DECREMENT } from './constant'
export default function countReducer(preState = 0, action) {
const { type, data } = action
switch (type) {
case INCREMENT:
return preState + data
case DECREMENT:
return preState - data
default:
return preState
}
}
// 组件中使用 Action Creator
import { createIncrementAction, createDecrementAction } from '../../redux/count_action'
increment = () => {
const { value } = this.selectNumber
store.dispatch(createIncrementAction(value * 1))
}
六、异步 Action(P102)
6.1 同步 Action vs 异步 Action
同步 Action:Action 的值是一个 Object
→ { type: 'increment', data: 1 }
异步 Action:Action 的值是一个 Function
→ 函数内部执行异步操作,然后再 dispatch 同步 action
6.2 使用 redux-thunk 中间件
npm install redux-thunk
// store.js — 应用中间件
import { createStore, applyMiddleware } from 'redux'
import thunk from 'redux-thunk'
import countReducer from './count_reducer'
export default createStore(countReducer, applyMiddleware(thunk))
// count_action.js — 新增异步 action
import { INCREMENT } from './constant'
// 同步 action(返回 Object)
export const createIncrementAction = data => ({ type: INCREMENT, data })
// 异步 action(返回 Function)
// 异步 action 中一般都会调用同步 action
export const createIncrementAsyncAction = (data, time) => {
return (dispatch) => {
setTimeout(() => {
dispatch(createIncrementAction(data))
}, time)
}
}
// 组件中使用
incrementAsync = () => {
const { value } = this.selectNumber
store.dispatch(createIncrementAsyncAction(value * 1, 500))
}
💡 异步 action 不是必须的,完全可以在组件中自己写异步逻辑再 dispatch 同步 action。但当异步逻辑需要复用时,异步 action 更方便。
七、react-redux(P103-P108)
7.1 为什么需要 react-redux?
原生 Redux 需要手动 subscribe、手动 getState、手动 dispatch,比较繁琐。Facebook 官方出品的 react-redux 库让 Redux 和 React 的结合更加优雅。
7.2 核心概念(P103)
react-redux 将组件分为两类:
UI 组件:
├── 只负责 UI 的呈现,不带有任何业务逻辑
├── 不使用任何 Redux 的 API
├── 通过 props 接收数据和操作函数
└── 一般保存在 components 目录
容器组件:
├── 负责管理数据和业务逻辑
├── 使用 Redux 的 API
├── 由 react-redux 的 connect 函数生成
└── 一般保存在 containers 目录
7.3 连接容器组件与 UI 组件(P104-P105)
npm install react-redux
// containers/Count/index.jsx — 容器组件
import { connect } from 'react-redux'
import CountUI from '../../components/Count' // UI 组件
import { createIncrementAction, createDecrementAction, createIncrementAsyncAction } from '../../redux/count_action'
// mapStateToProps:映射状态到 props
// 返回的对象中的 key 就作为传递给 UI 组件 props 的 key
// 返回的对象中的 value 就作为传递给 UI 组件 props 的 value — 状态
function mapStateToProps(state) {
return { count: state }
}
// mapDispatchToProps:映射操作状态的方法到 props
// 返回的对象中的 key 就作为传递给 UI 组件 props 的 key
// 返回的对象中的 value 就作为传递给 UI 组件 props 的 value — 操作状态的方法
function mapDispatchToProps(dispatch) {
return {
increment: data => dispatch(createIncrementAction(data)),
decrement: data => dispatch(createDecrementAction(data)),
incrementAsync: (data, time) => dispatch(createIncrementAsyncAction(data, time)),
}
}
// connect(映射状态, 映射方法)(UI组件)
export default connect(mapStateToProps, mapDispatchToProps)(CountUI)
// UI 组件中通过 props 使用
export default class Count extends Component {
increment = () => {
const { value } = this.selectNumber
this.props.increment(value * 1) // 通过 props 调用
}
render() {
return <h1>当前求和为:{this.props.count}</h1> // 通过 props 获取
}
}
// App.jsx — 给容器组件传入 store
import store from './redux/store'
import Count from './containers/Count'
export default class App extends Component {
render() {
return <Count store={store} />
}
}
⚠️ 重要:使用 react-redux 后,不需要再手动
subscribe了!connect 内部已经帮你做了状态监听和组件更新。
7.4 优化1:简写 mapDispatchToProps(P106)
mapDispatchToProps 可以简写为一个对象:
export default connect(
state => ({ count: state }),
// 简写:react-redux 会自动用 dispatch 包裹 action creator
{
increment: createIncrementAction,
decrement: createDecrementAction,
incrementAsync: createIncrementAsyncAction,
}
)(CountUI)
7.5 优化2:Provider 组件(P107)
不需要给每个容器组件手动传 store,用 Provider 统一提供:
// index.js
import { Provider } from 'react-redux'
import store from './redux/store'
ReactDOM.render(
<Provider store={store}>
<App />
</Provider>,
document.getElementById('root')
)
7.6 优化3:整合 UI 组件与容器组件(P108)
实际开发中,UI 组件和容器组件通常写在一个文件里:
// containers/Count/index.jsx — 合并写法
import { connect } from 'react-redux'
import { createIncrementAction, createDecrementAction, createIncrementAsyncAction } from '../../redux/count_action'
// UI 组件(不再单独导出)
class Count extends Component {
increment = () => {
const { value } = this.selectNumber
this.props.increment(value * 1)
}
render() {
return <h1>当前求和为:{this.props.count}</h1>
}
}
// 容器组件(导出)
export default connect(
state => ({ count: state }),
{
increment: createIncrementAction,
decrement: createDecrementAction,
incrementAsync: createIncrementAsyncAction,
}
)(Count)
八、数据共享(P109-P111)
8.1 多个 Reducer 的场景
当应用中有多个组件需要共享不同的数据时,需要多个 Reducer。
8.2 编写 Person 组件的 Reducer(P109-P110)
// redux/reducers/person.js
import { ADD_PERSON } from '../constant'
const initState = [{ id: '001', name: 'Tom', age: 18 }]
export default function personReducer(preState = initState, action) {
const { type, data } = action
switch (type) {
case ADD_PERSON:
return [data, ...preState] // 注意:不能用 push,必须返回新数组
default:
return preState
}
}
8.3 合并 Reducer(P111)
// redux/store.js
import { createStore, applyMiddleware, combineReducers } from 'redux'
import thunk from 'redux-thunk'
import countReducer from './reducers/count'
import personReducer from './reducers/person'
// combineReducers 合并多个 reducer
const allReducer = combineReducers({
count: countReducer,
persons: personReducer,
})
export default createStore(allReducer, applyMiddleware(thunk))
// Count 容器组件中获取状态
export default connect(
state => ({
count: state.count, // 取 count reducer 的状态
personCount: state.persons.length // 也可以取其他 reducer 的状态
}),
{ increment: createIncrementAction }
)(Count)
// Person 容器组件中获取状态
export default connect(
state => ({
persons: state.persons,
count: state.count // 也可以取 count 的状态
}),
{ addPerson: createAddPersonAction }
)(Person)
🎯 核心理解:使用
combineReducers后,store 中的 state 变成了一个对象,每个 key 对应一个 reducer 管理的状态片段。组件通过state.xxx获取对应的数据。
九、纯函数(P112)
9.1 什么是纯函数?
满足以下条件的函数:
- 同样的输入(实参),必定得到同样的输出(返回值)
- 不产生任何副作用(不修改外部变量、不发网络请求、不操作 DOM 等)
- 不依赖外部状态
// ✅ 纯函数
function add(a, b) { return a + b }
// ❌ 不是纯函数 — 依赖外部变量
let c = 10
function add(a, b) { return a + b + c }
// ❌ 不是纯函数 — 修改了输入参数(副作用)
function addItem(arr, item) {
arr.push(item) // 修改了原数组!
return arr
}
// ✅ 纯函数版本
function addItem(arr, item) {
return [...arr, item] // 返回新数组,不修改原数组
}
9.2 为什么 Reducer 必须是纯函数?
// ❌ 错误!push 修改了原数组,Redux 检测不到变化,不会触发更新
case ADD_PERSON:
preState.push(data)
return preState // 引用地址没变,Redux 认为状态没变
// ✅ 正确!返回新数组
case ADD_PERSON:
return [data, ...preState] // 新的引用地址,Redux 检测到变化
🎯 面试高频:为什么 Redux 的 Reducer 必须是纯函数? Redux 通过浅比较(比较引用地址)来判断 state 是否变化。如果直接修改原对象/数组(如 push),引用地址不变,Redux 认为状态没变,就不会通知组件更新。所以必须返回新的对象/数组。
十、Redux 开发者工具(P113)
10.1 安装
- Chrome 安装 Redux DevTools 扩展
- 项目中安装配套库:
npm install redux-devtools-extension
10.2 配置
// store.js
import { createStore, applyMiddleware, combineReducers } from 'redux'
import thunk from 'redux-thunk'
import { composeWithDevTools } from 'redux-devtools-extension'
import countReducer from './reducers/count'
import personReducer from './reducers/person'
const allReducer = combineReducers({
count: countReducer,
persons: personReducer,
})
// 用 composeWithDevTools 包裹 applyMiddleware
export default createStore(allReducer, composeWithDevTools(applyMiddleware(thunk)))
配置后打开 Chrome DevTools 的 Redux 面板,可以看到每次 dispatch 的 action、state 变化、时间旅行调试等。
十一、最终版项目结构(P114)
src/
├── redux/
│ ├── store.js → 创建 store,合并 reducer,应用中间件
│ ├── constant.js → action type 常量
│ ├── actions/
│ │ ├── count.js → count 的 action creator
│ │ └── person.js → person 的 action creator
│ └── reducers/
│ ├── index.js → 合并所有 reducer(combineReducers)
│ ├── count.js → count reducer
│ └── person.js → person reducer
├── containers/
│ ├── Count/index.jsx → Count 容器+UI 组件
│ └── Person/index.jsx → Person 容器+UI 组件
└── index.js → 入口,Provider 包裹 App
// redux/reducers/index.js — 统一合并 reducer
import { combineReducers } from 'redux'
import count from './count'
import persons from './person'
export default combineReducers({ count, persons })
// redux/store.js — 简化后
import { createStore, applyMiddleware } from 'redux'
import thunk from 'redux-thunk'
import { composeWithDevTools } from 'redux-devtools-extension'
import allReducer from './reducers'
export default createStore(allReducer, composeWithDevTools(applyMiddleware(thunk)))
十二、项目打包运行(P115)
# 打包
npm run build
# 使用 serve 库快速启动一个静态服务器
npm install -g serve
serve build
# 或
npx serve build
💡
serve是一个快速启动静态文件服务器的工具,适合本地预览打包后的项目。生产环境通常用 Nginx 部署。
本章知识图谱
Redux 状态管理
├── 核心概念
│ ├── Store:唯一的状态仓库
│ ├── Action:描述"发生了什么"的对象
│ ├── Reducer:纯函数,根据 action 返回新 state
│ └── 三大原则:单一数据源、state 只读、纯函数修改
├── 工作流程
│ └── Component → dispatch(action) → Store → Reducer → newState → Component
├── 异步 Action
│ ├── redux-thunk 中间件
│ └── action 返回函数而非对象
├── react-redux
│ ├── Provider:全局提供 store
│ ├── connect:连接组件与 store
│ ├── mapStateToProps:状态映射
│ ├── mapDispatchToProps:方法映射(可简写为对象)
│ └── 自动监听更新,无需手动 subscribe
├── 多 Reducer
│ ├── combineReducers 合并
│ └── state 变为对象,按 key 取值
├── 纯函数
│ ├── 同输入同输出,无副作用
│ └── Reducer 必须是纯函数(浅比较检测变化)
└── 开发工具
├── Redux DevTools 浏览器扩展
└── redux-devtools-extension 配套库
📌 下一篇:[React全家桶笔记(九):React扩展与Router 6] 最后一篇,涵盖 React Hooks、性能优化、错误边界,以及 React Router 6 的全新 API。