主题
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 后,组件树外面多了一个 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:

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 更容易定位问题。
课件图示








代码示例
示例 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 之后再处理。
常见错误
现象:点击加减按钮,
console里store.getState()的值变了,但页面数字纹丝不动。 原因:subscribe回调里只console.log了,没把getState().count写回 DOM;或者 DOM 只在初始化时写了一次。 修法:把视图更新语句放进subscribe回调内部(订阅一次,之后每次变化自动执行),不要在回调外面写。现象:
store.getState()返回undefined,随后访问.count直接报错。 原因:reducer的switch没有写default: return state,遇到未匹配的 action(含 Redux 初始化时派发的内部 action)就返回了undefined。 修法:补上default分支返回原state;同时给第一个参数写上默认值state = { count: 0 }。现象:点了按钮,
subscribe完全不触发,也没报错。 原因:dispatch的action.type与reducer里case的字符串不一致(大小写、单复数、拼写,如'increment'vs'INCREMENT')。 修法:把 type 抽成常量或从slice.actions里解构使用,避免手写字符串;调试时先console.log(action.type)对照case。现象:纯 Redux 版本里
state.count++后return state,getState()数值确实变了,但某些订阅/对比逻辑判断不出变化。 原因:直接修改原对象并返回同一引用,违反"数据不可变",靠引用比较的地方认为没变。 修法:return { ...state, count: state.count + 1 }。(在 RTK 的createSlice里因内置 immer 才允许直接写state.count++,两处规则不要混用。)现象: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。现象:
useSelector(state => state.counter.count)拿到undefined,组件渲染空白。 原因:configureStore({ reducer: { counter: counterReducer } })里的 key 名与useSelector里的state.xxx不一致。 修法:useSelector的路径必须严格对应configureStore中reducer的键名,键名即模块名。现象:
useSelector里返回{ count }或.map()出的新数组,组件疯狂重渲染直至报 "Maximum update depth" / "getSnapshot should be cached"。 原因:每次调用都返回新引用,react-redux认为状态一直在变。 修法:一次只取一个原始值(state.counter.count),或用useSelector(..., shallowEqual),或分多次useSelector调用。现象:组件里
useSelector抛 "could not find react-redux context value"。 原因:<Provider store={store}>没包住该组件,或store属性名写成了别的。 修法:在index.js的render中用<Provider store={store}>包裹根组件,属性名固定为store。现象:异步列表拿不到数据,
payload是undefined。 原因:dispatch时忘了把 thunk 函数return出来(写成{ fetchChannelList() }或漏写return),或接口返回值层级取错(应为res.data.data.channels之类)。 修法:thunk 必须return (dispatch) => {...};取值前先console.log(res)确认层级。现象:
createSlice的reducers里发请求、await,状态时好时坏。 原因:reducers只放同步方法,异步逻辑不能写在这里。 修法:同步改数据放reducers(如setChannels),异步流程单独写 thunk 函数,内部再dispatch同步 action。
练习
补全纯 Redux 计数器:给定
counterReducer的switch只写了INCREMENT分支,请补全DECREMENT、RESET(重置为 0)以及default,并保证dispatch({type:'RESET'})后页面显示 0。排查题:下面代码点击按钮无反应,请指出至少两处问题并改正。
jsconst 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 } }RTK 改造:把练习 1 的纯 Redux 计数器用
createSlice+configureStore重写,并在 React 组件里用useSelector/useDispatch完成渲染与增减;要求configureStore的模块键名为counter。传参练习:在计数器上增加
+10与设为指定值两个按钮,用payload传递参数,写出reducers中的方法、dispatch的调用以及点击后count的期望值。异步练习:用 thunk 从接口获取 channel 列表并渲染。要求写出:
slice中的同步setChannels、thunk 函数、组件里useEffect内的dispatch调用,并说明payload该赋什么值。
原视频对照
| # | 视频 | 时长 |
|---|---|---|
| 1 | Redux快速上手 | 16:49 |
| 2 | Redux与React-环境准备 | 08:06 |
| 3 | Redux与React-实现counter | 20:32 |
| 4 | Redux与React-提交action传参 | 05:53 |
| 5 | Redux与React-异步状态操作 | 14:47 |
| 6 | Redux调试-devtools | 03:59 |
| 7 | 美团案例-案例演示和环境准备 | 07:44 |
小结
- Redux 三件套的职责边界要记死:
state存数据、action描述怎么改、reducer根据action.type产出新 state。 - 纯 Redux 必须遵守不可变更新(返回新对象),RTK 的
createSlice因内置 immer 才允许"直接改",两套规则不要互相污染。 - 修改数据的唯一入口是
dispatch;视图更新靠subscribe(原生)或useSelector(React)自动完成。 configureStore中reducer的键名 = 组件里useSelector(state.键名)的键名,这是最高频的"数据拿不到"来源。- action creator 必须调用后再交给
dispatch,它的返回值才是 action 对象;带参时参数落在action.payload。 - 异步逻辑一律放 thunk(外层函数返回内层函数),内层负责"取数据 + dispatch 同步 action",
reducers里只留同步方法。 useSelector一次只取最小粒度的原始值,返回新对象/新数组会引发无谓甚至无限重渲染。- 调试优先用 Redux DevTools 看 action 列表与 state 树,比
console.log更快定位"action 没发出去"还是"reducer 没接住"。