Skip to content

Redux 与状态管理(一) ​

本页对应 Redux 与状态管理,共 7 段视频,约 1 小时 17 分。

学习目标 ​

  • 能够说明 Redux 作为集中状态管理工具的定位,并解释 Store 独立于组件树、供任意组件读写共享状态的机制
  • 能够在不依赖任何框架的纯 JavaScript 环境中,独立完成「定义 reducer → createStore → subscribe → dispatch → getState」五步,实现一个可运行的计数器,并说明 reducer 必须返回新状态对象(数据不可变)的原因
  • 能够用 createSlice 定义子模块(name / initialState / reducers),用 configureStore 组合根 store,并通过 Provider 注入、useSelector 读取状态、useDispatch 提交 action,完成 React 版计数器
  • 能够借助 action creator 传参,使参数自动写入 action 的 payload 属性,并在 reducer 中读取该参数完成“直接跳转到指定值”的功能
  • 能够封装「外层函数返回内层函数」的异步 action,在内层函数中发起请求、调用同步 action creator 并通过 dispatch 提交,再在组件的 useEffect 中触发,把后端数据写入 store 并渲染列表
  • 能够安装并使用 Redux DevTools 查看 action 提交记录、各模块状态快照与时间旅行调试,定位状态更新问题;并能在美团案例的现成环境中启动前端与 mock 服务

知识点 ​

Redux是什么:集中状态管理工具 ​

为什么需要:当应用里的用户信息、购物车、商品列表等状态被多个组件共同使用时,如果只靠组件之间层层传参,通信会变得混乱,组件层级越深越难维护。Redux 就是为解决这类“全局状态集中管理”问题而出现的。

怎么想:先把它类比成 Vue 生态里的 Pinia(或 Vuex):它们都不是某个组件内部的小状态,而是把应用级状态集中放到一个地方管理。Redux 还有一个重要特点:它可以独立于框架运行。虽然它最常和 React 搭配,但不绑定 React 也能自己跑起来。

怎么做:记住一句话:Redux 通过集中管理的方式管理应用状态。它适合管理“全局都需要使用”的状态,例如用户信息、购物车数据等。

易错点:不要把 Redux 理解成“所有状态都必须放进去”。组件内部的临时状态、简单交互状态,不一定需要 Redux;否则会把简单问题复杂化。

为什么要使用Redux:三个核心价值 ​

为什么需要:理解一个工具,先要知道它解决什么问题。转写稿里明确提到 Redux 的三个价值,这三点就是学习时的判断标准。

怎么想:第一,独立于组件,无视组件之间的层级关系,简化通信问题;第二,单向数据流清晰,易于定位 bug;第三,调试工具配套良好,方便调试。注意笔记里“单项数据流”应理解为“单向数据流”。

怎么做:当你要在多个层级、多个组件之间共享和修改同一份数据时,优先考虑 Redux;当出现数据流混乱、排查状态变化困难时,也可以借助 Redux 的单向数据流和调试工具。

易错点:不要把 Redux 当成“万能状态库”。它的优势是集中管理和可调试,但也会带来样板代码;如果状态只在父子组件之间使用,普通 props 或组件状态可能更直接。

用图示理解:Store 独立于组件树 ​

Redux集中状态管理示意图

为什么需要:只看定义容易抽象,图示能帮助理解 Redux 到底改变了什么。

怎么想:看这张图时,先对比“没有 Redux”和“使用 Redux”两种情况。没有 Redux 时,组件树中的组件之间通信比较混乱;使用 Redux 后,组件树外面多了一个 store。store 独立于整个组件树,因此组件树里的任何一个组件都可以使用 store 里的数据,也可以通过固定方式修改 store 里的数据。

怎么做:把 store 想成“公共数据仓库”。组件不再互相绕来绕去传数据,而是统一从 store 取数据;修改时也统一走 Redux 规定的流程。一旦 store 中的数据发生变化,所有用到该数据的组件都会得到更新。

易错点:不要把 store 理解成某个组件的子状态。它位于组件树之外,是集中管理的中枢;组件只是使用者和触发修改者。

Redux最小工作流:五步先跑起来 ​

为什么需要:转写稿强调,初学 Redux 会觉得步骤多、样板代码多,所以先用一个最朴素的计数器案例把流程跑通,有助于后面理解 React 集成。

怎么想:先用“独立运行”的方式理解 Redux,不急着上 React。计数器需求很简单:点减号减一,点加号加一,并显示当前 count。实现时围绕五个步骤:定义 reducer、创建 store、订阅变化、提交 action、获取最新状态并更新视图。

怎么做:按转写稿口述,关键流程可以整理为下面这种示意代码(本节笔记未给完整代码,这里只按口述整理关键片段):

js
function reducer(state = { count: 0 }, action) {
  if (action.type === 'increment') {
    return { count: state.count + 1 }
  }
  if (action.type === 'decrement') {
    return { count: state.count - 1 }
  }
  return state
}

const store = createStore(reducer)

store.subscribe(() => {
  console.log('state变化了')
  document.getElementById('count').innerText = store.getState().count
})

store.dispatch({ type: 'increment' })
store.dispatch({ type: 'decrement' })

调试时,转写稿的做法是:点击加号、减号,观察 subscribe 回调是否打印“state变化了”;再在回调里用 getState 拿到最新 count,并写入页面元素。这样能验证“提交 action → reducer 返回新状态 → 订阅回调执行 → 视图更新”的闭环。

易错点:第一,修改数据的唯一方式是 dispatch 一个 action,不要直接改 store;第二,reducer 必须基于原状态返回新状态,不能直接修改原 state;第三,action.type 要和 reducer 中的判断匹配;第四,subscribe 只负责通知变化,更新视图还要通过 getState 取最新状态。

核心概念与易错总结 ​

为什么需要:五步跑通后,要把流程抽象成概念,否则只会照抄代码。

怎么想:Redux 为了职责清晰,把数据修改流程分成三个核心概念:state 是存放要管理的数据状态的对象;action 是描述“想怎么修改数据”的对象;reducer 是根据 action 的描述生成最新 state 的函数。dispatch 提交 action,reducer 产生新 state,组件再消费新 state。

怎么做:写 Redux 时先问自己三件事:我要管理什么 state?我要通过什么 action 描述修改?reducer 如何根据 action 返回新 state?把这三件事对应清楚,流程就不会乱。

易错点:不要把 action 当成“直接修改函数”,它只是描述修改;不要绕过 reducer 改数据;不要忘记 reducer 的默认返回。初学阶段先接受样板代码,等流程熟悉后,再学习后续的 Redux Toolkit 简化写法。

Redux是什么:集中状态管理工具 ​

为什么需要:组件之间共享的用户信息、购物车、分类列表等状态,如果靠父子传参和事件回调维护,组件树一复杂,通信就会混乱。Redux 用来做集中状态管理,把全局都要用的状态放到统一位置管理。

怎么想:先把它理解成 React 生态里最常用的集中状态管理工具,类似 Vue 里的 Vuex/Pinia;但它有个重要特点:可以独立于框架运行。所以本节先不接 React,单独用 Redux 跑一个计数案例,降低理解成本。

怎么做:观察原笔记中的示意图:未使用 Redux 时,组件之间通信较乱;使用 Redux 后,组件树外多了一个 store。任何组件都能从 store 读数据,也能通过固定方式修改数据;数据一变,用到数据的组件就更新。

易错点:不要把 Redux 理解成只能给 React 用;也不要一上来就把它和组件绑定。先独立跑通,才能看清 store、action、reducer 各自负责什么。

先脱离React跑起来:计数案例 ​

为什么需要:只看概念容易虚,必须用一个最小案例把数据流跑通。计数案例有点减、点加、显示当前 count,刚好覆盖“读状态、改状态、更新视图”。

怎么想:页面只做三件事:按钮触发修改,Redux 内部根据修改描述生成新状态,订阅回调拿到新状态后更新视图。先不引入 React,直接用原生 DOM 操作。

怎么做:准备两个按钮和一个显示 count 的 span。按五个步骤写:定义 reducer、createStore 生成 store、subscribe 订阅变化、dispatch 提交 action、getState 获取最新状态并渲染。

易错点:不要把 dispatch 写成直接改 store 内部数据;修改数据的唯一方式是 dispatch 一个 action 对象。type 字符串必须和 reducer 里的判断一致。

五个步骤:reducer→store→subscribe→dispatch→getState ​

为什么需要:Redux 的样板代码较多,初学时要先按固定步骤建立肌肉记忆,再理解每一步的职责。

怎么想:reducer 负责“根据 action 返回新 state”;createStore 负责“生成 store 实例”;subscribe 负责“监听变化”;dispatch 负责“提交 action 触发修改”;getState 负责“取最新状态”。

怎么做:

js
import { createStore } from 'redux';

// 1. 定义 reducer:根据 action.type 返回新状态
const reducer = (state = { count: 0 }, action) => {
  switch (action.type) {
    case 'increment':
      return { count: state.count + 1 };
    case 'decrement':
      return { count: state.count - 1 };
    default:
      return state;
  }
};

// 2. 生成 store 实例
const store = createStore(reducer);

// 3. 订阅状态变化
store.subscribe(() => {
  console.log('state变化了');
  document.getElementById('count').innerText = store.getState().count;
});

// 4. 提交 action
document.getElementById('inc').addEventListener('click', () => {
  store.dispatch({ type: 'increment' });
});
document.getElementById('dec').addEventListener('click', () => {
  store.dispatch({ type: 'decrement' });
});

// 5. 获取最新状态并更新视图
document.getElementById('count').innerText = store.getState().count;

调试时可以先在 subscribe 里打印 store.getState():点加号,控制台输出 count 为 1、2;点减号,count 回到 1、0。确认状态确实变了,再把 count 写到页面 span 中。

易错点:reducer 不能直接写 state.count++,要基于原状态返回新对象,这叫“数据不可变”;default 要返回原 state;dispatch 传入的是 action 对象,不是字符串。

核心概念闭环:state、action、reducer ​

为什么需要:五个步骤只是操作顺序,真正要掌握的是数据流闭环。否则代码会写,但不知道为什么这样写。

怎么想:state 是存放管理数据的对象;action 是描述“想怎么改”的对象,通常带 type;reducer 是函数,根据 action 生成最新 state。视图通过 dispatch 提交 action,action 进入 reducer,reducer 产生新 state,订阅回调再把新 state 反映到视图。

怎么做:对照代码走一遍:按钮点击 dispatch({ type: 'increment' }),reducer 匹配 increment,返回 { count: state.count + 1 },subscribe 回调执行,getState() 取到新值,更新页面。这样就读写闭环了。

易错点:把 action 当成“直接修改函数”是错的,它只是描述;真正计算新状态的是 reducer。getState 只负责取,不负责改。

快速体验中的调试与易错点 ​

为什么需要:Redux 流程比普通变量修改长,出错时要定位是 action 没提交、type 不匹配,还是视图没订阅。

怎么想:按链路排查:按钮是否触发 dispatch → action 的 type 是否正确 → reducer 是否命中分支 → subscribe 是否执行 → getState 是否取到新值 → DOM 是否更新。

怎么做:先在 subscribe 中打印 'state变化了' 和 store.getState();确认无误后再更新 innerText。本节完成后,应能独立说出 store、state、action、reducer 的关系。

易错点:同音字要纠正为 reducer、createStore、subscribe、dispatch、getState、action、increment、decrement;不要把 subscribe 写成 subscript,不要把 dispatch 写成 dispense。本节只是快速体验,下一节再把 Redux 接入 React。

先明确目标:不绑框架的计数器 ​

这一节我们要用纯 Redux 实现一个计数器:点“-”数字减一,点“+”数字加一。需求里特别强调:不和任何框架绑定,不使用任何构建工具。为什么需要这样安排?因为 Redux 虽然是 React 生态里最常用的集中状态管理工具,但它本身可以独立运行。先脱离 React 跑通最小案例,才能把 store、reducer、action、dispatch、subscribe、getState 这些核心概念看清楚,后面接 React 时就不会把框架语法和 Redux 本身混在一起。

怎么想?页面只保留三个元素:减号按钮、显示数字的 span、加号按钮;Redux 只负责“存 count、改 count、通知视图”。怎么做?先按笔记给出的界面搭 HTML,再用 script 标签引入 Redux:

纯 Redux 计数器界面

html
<button id="decrement">-</button>
<span id="count">0</span>
<button id="increment">+</button>

<script src="https://unpkg.com/redux@latest/dist/redux.min.js"></script>

易错点:span 的 id 是 count,后面 getElementById 必须一致;引入的是 Redux 的浏览器构建文件,别把 Redux 写成小写;这一节先不要引入 React。

第一步:定义 reducer,把“怎么改”收口 ​

为什么需要 reducer?因为 Redux 不允许视图随便改数据,必须有一个函数根据“你想做什么”返回新状态。怎么想?把它想成一个“状态加工厂”:接收当前 state 和 action,根据 action.type 决定返回什么新状态。怎么做?给 state 一个默认初始值 { count: 0 },用 switch 判断 INCREMENT 和 DECREMENT,最后 default 返回原 state。

js
function counterReducer (state = { count: 0 }, action) {
  switch (action.type) {
    case 'INCREMENT':
      return { count: state.count + 1 }
    case 'DECREMENT':
      return { count: state.count - 1 }
    default:
      return state
  }
}

易错点:不要写成 state.count++ 或 state.count--。转写稿里反复强调“数据不可变”,必须基于原状态生成新对象;action.type 的字符串要和后面 dispatch 的 type 完全匹配,大小写不能错;default 必须返回 state,否则 store 初始化时可能拿到 undefined。

第二步:createStore 生成 store 实例 ​

为什么需要 store?reducer 只是规则,store 才是真正保存状态、接收 dispatch、提供订阅和读取能力的实例。怎么想?把 reducer 交给 Redux.createStore,让它创建一个“仓库”。怎么做?

js
const store = Redux.createStore(counterReducer)

易错点:传入的是 counterReducer 函数本身,不是 counterReducer() 的调用结果;createStore 是 Redux 提供的方法,注意大小写。

第三步:subscribe 订阅数据变化 ​

为什么需要订阅?因为数据变了以后,视图得知道。转写稿里的调试过程很典型:先不急着渲染,先在订阅回调里打印日志,验证“数据变化时回调会不会自动执行”。怎么想?subscribe 注册一个回调,每次 state 变化后自动执行。怎么做?

js
store.subscribe(() => {
  console.log(store.getState())
  document.getElementById('count').innerText = store.getState().count
})

易错点:subscribe 只是注册,不会立刻执行;不要在回调里再 dispatch 造成循环;回调里要通过 getState() 拿最新状态,而不是依赖外部旧变量。

第四步:dispatch 提交 action 触发修改 ​

为什么需要 dispatch?Redux 里修改数据的唯一方式就是提交 action。怎么想?按钮点击时,告诉 store “我要增/减”,至于怎么增、怎么减,由 reducer 决定。怎么做?给两个按钮绑定点击事件,分别提交 { type: 'INCREMENT' } 和 { type: 'DECREMENT' }。

js
const inBtn = document.getElementById('increment')
inBtn.addEventListener('click', () => {
  store.dispatch({ type: 'INCREMENT' })
})

const dBtn = document.getElementById('decrement')
dBtn.addEventListener('click', () => {
  store.dispatch({ type: 'DECREMENT' })
})

易错点:action 必须是对象,并且有 type;type 拼错时 reducer 会走 default,页面看起来“点了没反应”;不要直接改 store.getState().count。

第五步:getState 获取最新状态并更新视图 ​

为什么需要 getState?因为 store 里的状态不能直接读,要通过方法取。怎么想?在 subscribe 回调里,数据一变就取最新 count,写到 span 上。转写稿的调试顺序是:先打印 store.getState(),点加号看到 count 为 1、再点为 2,点减号回到 1、0,确认状态对了,再把 innerText 渲染上去。易错点:本例初始值刚好是 0,HTML 里也写了 0;如果以后初始状态不是 0,要注意首次渲染。DOM id 不要写错,innerText 赋值的是 store.getState().count。

完整代码与闭环理解 ​

html
<button id="decrement">-</button>
<span id="count">0</span>
<button id="increment">+</button>

<script src="https://unpkg.com/redux@latest/dist/redux.min.js"></script>

<script>
  // 定义reducer函数 
  // 内部主要的工作是根据不同的action 返回不同的state
  function counterReducer (state = { count: 0 }, action) {
    switch (action.type) {
      case 'INCREMENT':
        return { count: state.count + 1 }
      case 'DECREMENT':
        return { count: state.count - 1 }
      default:
        return state
    }
  }
  // 使用reducer函数生成store实例
  const store = Redux.createStore(counterReducer)

  // 订阅数据变化
  store.subscribe(() => {
    console.log(store.getState())
    document.getElementById('count').innerText = store.getState().count

  })
  // 增
  const inBtn = document.getElementById('increment')
  inBtn.addEventListener('click', () => {
    store.dispatch({
      type: 'INCREMENT'
    })
  })
  // 减
  const dBtn = document.getElementById('decrement')
  dBtn.addEventListener('click', () => {
    store.dispatch({
      type: 'DECREMENT'
    })
  })
</script>

这套流程是一个闭环:视图点击按钮 → dispatch 提交 action → reducer 根据 action.type 返回新 state → subscribe 回调被触发 → getState 取最新 count → 更新视图。先按“打印日志验证状态变化,再渲染视图”的顺序调试,比一上来就写 DOM 更容易定位问题。

课件图示 ​

课件第18页:提交action传参实现需求 在reducers的同步修改方法中添加action

课件第3页:不和任何框架绑定,不使用任何构建工具,使用纯Redux实现计数器 使用步骤: 1

课件第21页:异步操作样板代码 1. 创建store的写法保持不变,配置好同步修改状态的方法

课件第25页:基本开发思路:使用 RTK(Redux Toolkit)来管理应用状态, 组件负

课件第29页:实现步骤 启动项目(mock服务 + 前端服务 ) 使用RTK编写store(异

课件第43页:实现步骤 RTK中增加增减reducer,组件中提交 action 使用cart

课件第6页:在React中使用redux,官方要求安装俩个其他插件 - Redux Tool

课件第14页:React组件修改store中的数据 React组件中修改store中的数据需要

代码示例 ​

示例 1:纯 Redux 实现计数器(不绑定任何框架、不用构建工具)

html
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>Redux 计数器</title>
</head>
<body>
  <button id="decrement">-</button>
  <span id="count">0</span>
  <button id="increment">+</button>

  <!-- 通过 CDN 引入 redux,暴露全局 Redux -->
  <script src="https://unpkg.com/redux@latest/dist/redux.min.js"></script>

  <script>
    // 1. 定义 reducer 函数
    //    内部主要的工作是根据不同的 action 返回不同的 state
    function counterReducer (state = { count: 0 }, action) {
      switch (action.type) {
        case 'INCREMENT':
          return { count: state.count + 1 }
        case 'DECREMENT':
          return { count: state.count - 1 }
        default:
          return state
      }
    }

    // 2. 使用 reducer 函数生成 store 实例
    const store = Redux.createStore(counterReducer)

    // 3. 订阅数据变化(数据一旦变化,回调自动执行)
    store.subscribe(() => {
      console.log(store.getState())
      document.getElementById('count').innerText = store.getState().count
    })

    // 4. 增:dispatch 提交 action 对象
    const inBtn = document.getElementById('increment')
    inBtn.addEventListener('click', () => {
      store.dispatch({
        type: 'INCREMENT'
      })
    })

    // 4. 减:dispatch 提交 action 对象
    const dBtn = document.getElementById('decrement')
    dBtn.addEventListener('click', () => {
      store.dispatch({
        type: 'DECREMENT'
      })
    })

    // 5. getState 获取最新状态已在 subscribe 回调中完成,用于更新视图
  </script>
</body>
</html>

要点解释与易错点:

  • counterReducer(state = { count: 0 }, action) 的默认参数只在 state 为 undefined 时生效,Redux 内部初始化时会 dispatch 一个 type 随机的 action,正好走进 default 分支拿到初始状态,所以初始值一定要写在形参默认值上,不要在函数体里再写一遍。
  • reducer 必须返回新对象:return { count: state.count + 1 },不能写成 state.count++ 或 state.count = state.count + 1。这就是“数据不可变”,直接改原对象视图和调试工具都不会正确响应。
  • default: return state 不能漏,否则第一次初始化时返回 undefined,store 会直接报错。
  • 修改数据的唯一方式是 store.dispatch(action对象),action 必须是一个带 type 属性的对象,且 type 的值要和 reducer 里 case 的字符串完全一致(大小写、拼写)。
  • store.subscribe(回调) 的回调在每次 state 变化后自动执行,视图更新逻辑(document.getElementById('count').innerText = ...)要写在这里,而不是写在按钮点击里。
  • store.getState() 返回的是最新的状态对象,取数值要写 .count,别直接把它当数字用。
  • 如果 CDN 拉到的是最新版 redux,createStore 属于旧版 API(会有废弃提示);若控制台报 createStore is not a function,把地址换成 4.x 版本即可,本节逻辑不变。

示例 2:单独验证 reducer 的“纯函数 / 不可变”行为(脱离 Redux 也能跑)

html
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>验证 reducer</title>
</head>
<body>
  <pre id="log"></pre>

  <script>
    // 与示例 1 完全一致的 reducer
    function counterReducer (state = { count: 0 }, action) {
      switch (action.type) {
        case 'INCREMENT':
          return { count: state.count + 1 }
        case 'DECREMENT':
          return { count: state.count - 1 }
        default:
          return state
      }
    }

    const log = document.getElementById('log')
    const print = (title, state) => {
      log.innerText += title + ' => ' + JSON.stringify(state) + '\n'
    }

    // 1. 初始状态:state 传 undefined,走形参默认值
    const s0 = counterReducer(undefined, { type: '@@INIT' })
    print('初始状态', s0)

    // 2. 增
    const s1 = counterReducer(s0, { type: 'INCREMENT' })
    print('INCREMENT', s1)

    // 3. 减
    const s2 = counterReducer(s1, { type: 'DECREMENT' })
    print('DECREMENT', s2)

    // 4. 未知 action,返回原 state
    const s3 = counterReducer(s2, { type: 'UNKNOWN' })
    print('未知 action', s3)

    // 5. 验证不可变:原状态没被改,且新状态是新对象
    log.innerText += '\n原状态 s0 是否被改动:' + JSON.stringify(s0) + '\n'
    log.innerText += 's0 === s1 ?' + (s0 === s1) + '\n'
    log.innerText += 's2 === s3 ?(未知 action 返回原引用)' + (s2 === s3)
  </script>
</body>
</html>

要点解释与易错点:

  • 这个例子把 reducer 当成普通纯函数来调用,能直观看出“同一输入必得同一输出、且不修改入参”,这是 Redux 能配合调试工具做时间旅行的前提。
  • 第一次调用传 undefined 作为 state,才会触发 state = { count: 0 } 的默认值;如果传 null,默认参数不会生效,会直接崩掉。
  • 命中 default 时返回的是原来的引用(s2 === s3 为 true),Redux 据此判断“状态没变”,不会通知订阅者;而增/减分支返回的是新对象,引用变了才会通知。
  • 常见错误:在 reducer 里写 state.count = action.payload 后再 return state,此时引用没变,subscribe 的回调不会执行,视图不更新——这类“视图不动”的 bug 基本都出在这里。
  • 另一个易错点:reducer 里不能写异步代码、不能发请求,它只负责“根据 action 算出新 state”,异步逻辑要放到组件里 dispatch 之后再处理。

常见错误 ​

  1. 现象:点击加减按钮,console 里 store.getState() 的值变了,但页面数字纹丝不动。 原因:subscribe 回调里只 console.log 了,没把 getState().count 写回 DOM;或者 DOM 只在初始化时写了一次。 修法:把视图更新语句放进 subscribe 回调内部(订阅一次,之后每次变化自动执行),不要在回调外面写。

  2. 现象:store.getState() 返回 undefined,随后访问 .count 直接报错。 原因:reducer 的 switch 没有写 default: return state,遇到未匹配的 action(含 Redux 初始化时派发的内部 action)就返回了 undefined。 修法:补上 default 分支返回原 state;同时给第一个参数写上默认值 state = { count: 0 }。

  3. 现象:点了按钮,subscribe 完全不触发,也没报错。 原因:dispatch 的 action.type 与 reducer 里 case 的字符串不一致(大小写、单复数、拼写,如 'increment' vs 'INCREMENT')。 修法:把 type 抽成常量或从 slice.actions 里解构使用,避免手写字符串;调试时先 console.log(action.type) 对照 case。

  4. 现象:纯 Redux 版本里 state.count++ 后 return state,getState() 数值确实变了,但某些订阅/对比逻辑判断不出变化。 原因:直接修改原对象并返回同一引用,违反"数据不可变",靠引用比较的地方认为没变。 修法:return { ...state, count: state.count + 1 }。(在 RTK 的 createSlice 里因内置 immer 才允许直接写 state.count++,两处规则不要混用。)

  5. 现象:RTK 项目里点按钮报 Actions must be plain objects. Use custom middleware for async actions.原因:dispatch(increment) 忘了加括号,把 action creator 函数本身派发出去了;或 dispatch(axios.get(url)) 直接派发 Promise。 修法:dispatch(increment());异步一律封装成 thunk(return (dispatch) => {...})再 dispatch。

  6. 现象:useSelector(state => state.counter.count) 拿到 undefined,组件渲染空白。 原因:configureStore({ reducer: { counter: counterReducer } }) 里的 key 名与 useSelector 里的 state.xxx 不一致。 修法:useSelector 的路径必须严格对应 configureStore 中 reducer 的键名,键名即模块名。

  7. 现象:useSelector 里返回 { count } 或 .map() 出的新数组,组件疯狂重渲染直至报 "Maximum update depth" / "getSnapshot should be cached"。 原因:每次调用都返回新引用,react-redux 认为状态一直在变。 修法:一次只取一个原始值(state.counter.count),或用 useSelector(..., shallowEqual),或分多次 useSelector 调用。

  8. 现象:组件里 useSelector 抛 "could not find react-redux context value"。 原因:<Provider store={store}> 没包住该组件,或 store 属性名写成了别的。 修法:在 index.js 的 render 中用 <Provider store={store}> 包裹根组件,属性名固定为 store。

  9. 现象:异步列表拿不到数据,payload 是 undefined。 原因:dispatch 时忘了把 thunk 函数 return 出来(写成 { fetchChannelList() } 或漏写 return),或接口返回值层级取错(应为 res.data.data.channels 之类)。 修法:thunk 必须 return (dispatch) => {...};取值前先 console.log(res) 确认层级。

  10. 现象:createSlice 的 reducers 里发请求、await,状态时好时坏。 原因:reducers 只放同步方法,异步逻辑不能写在这里。 修法:同步改数据放 reducers(如 setChannels),异步流程单独写 thunk 函数,内部再 dispatch 同步 action。

练习 ​

  1. 补全纯 Redux 计数器:给定 counterReducer 的 switch 只写了 INCREMENT 分支,请补全 DECREMENT、RESET(重置为 0)以及 default,并保证 dispatch({type:'RESET'}) 后页面显示 0。

  2. 排查题:下面代码点击按钮无反应,请指出至少两处问题并改正。

    js
    const store = Redux.createStore(reducer)
    store.subscribe(() => console.log('changed'))
    btn.onclick = () => store.dispatch({ type: 'increament' })
    
    function reducer(state, action) {
      switch (action.type) {
        case 'INCREMENT': state.count++; return state
      }
    }
  3. RTK 改造:把练习 1 的纯 Redux 计数器用 createSlice + configureStore 重写,并在 React 组件里用 useSelector / useDispatch 完成渲染与增减;要求 configureStore 的模块键名为 counter。

  4. 传参练习:在计数器上增加 +10 与 设为指定值 两个按钮,用 payload 传递参数,写出 reducers 中的方法、dispatch 的调用以及点击后 count 的期望值。

  5. 异步练习:用 thunk 从接口获取 channel 列表并渲染。要求写出:slice 中的同步 setChannels、thunk 函数、组件里 useEffect 内的 dispatch 调用,并说明 payload 该赋什么值。

原视频对照 ​

#视频时长
1Redux快速上手16:49
2Redux与React-环境准备08:06
3Redux与React-实现counter20:32
4Redux与React-提交action传参05:53
5Redux与React-异步状态操作14:47
6Redux调试-devtools03:59
7美团案例-案例演示和环境准备07:44

小结 ​

  1. Redux 三件套的职责边界要记死:state 存数据、action 描述怎么改、reducer 根据 action.type 产出新 state。
  2. 纯 Redux 必须遵守不可变更新(返回新对象),RTK 的 createSlice 因内置 immer 才允许"直接改",两套规则不要互相污染。
  3. 修改数据的唯一入口是 dispatch;视图更新靠 subscribe(原生)或 useSelector(React)自动完成。
  4. configureStore 中 reducer 的键名 = 组件里 useSelector(state.键名) 的键名,这是最高频的"数据拿不到"来源。
  5. action creator 必须调用后再交给 dispatch,它的返回值才是 action 对象;带参时参数落在 action.payload。
  6. 异步逻辑一律放 thunk(外层函数返回内层函数),内层负责"取数据 + dispatch 同步 action",reducers 里只留同步方法。
  7. useSelector 一次只取最小粒度的原始值,返回新对象/新数组会引发无谓甚至无限重渲染。
  8. 调试优先用 Redux DevTools 看 action 列表与 state 树,比 console.log 更快定位"action 没发出去"还是"reducer 没接住"。