React全家桶笔记(八):Redux与React-Redux状态管理

3 阅读10分钟

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 什么是纯函数?

满足以下条件的函数:

  1. 同样的输入(实参),必定得到同样的输出(返回值)
  2. 不产生任何副作用(不修改外部变量、不发网络请求、不操作 DOM 等)
  3. 不依赖外部状态
// ✅ 纯函数
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 安装

  1. Chrome 安装 Redux DevTools 扩展
  2. 项目中安装配套库:
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。