主题
reduxjs/toolkit 小结与附录(Day9 2/3)
本页对应 React + TypeScript + 医通项目,共 5 段视频,约 48 分钟。
学习目标
- 能说明仅使用 store.dispatch 修改仓库数据后 React 界面不更新的原因,并完成 react-redux 的安装、Provider 包裹根组件及绑定 store。
- 能使用 useSelector 读取整个仓库、指定切片及切片字段,并使用 useDispatch 创建 dispatch 函数触发 action creator 修改状态。
- 能使用 BrowserRouter、Routes、Route、Navigate 和 Link 配置添加商品、商品列表、购物车列表路由及默认重定向,完成静态页面布局与导航切换。
- 能按模块化目录创建 goodsSlice、cartSlice 和 store:定义 name、initialState、reducers,默认导出 reducer、分别导出 action creator,并用 configureStore 注册 reducer。
- 能在添加商品表单中阻止默认提交、通过 ref 获取商品名和价格、构造 payload,dispatch 商品切片的添加 action,生成 id 并追加到 goods 数组,提交后编程式跳转到商品列表。
- 能在商品列表页使用 useSelector 获取 goods 数组,通过 map 渲染商品名、价格和加入购物车按钮,并验证新增商品实时显示。
知识点
一、store:用 configureStore 汇总切片
为什么需要:一个项目会有多个业务切片,必须有一个仓库把它们装在一起,组件才能统一读取和修改。怎么想:configureStore 的 reducer 配置对象里,每个 key 就是以后 state 上的字段名。怎么做:笔记里的 store 写法要保留:
js
const store = configureStore({
reducer:{
count
}
})购物车案例里就是 goods、cart 两个切片放进 reducer。易错点:reducer 里放的是切片导出的 reducer,不是整个 slice;key 写 goods,useSelector 里就要读 state.goods。转写稿里“故ose/goose”应纠正为 goods,“car”应纠正为 cart。
二、slice:状态、同步 actionCreator、异步入口
为什么需要:按模块拆状态,避免所有逻辑堆在一个文件。怎么想:一个 slice 记五件事:name、state、reducer(同步 reducer,同步 actionCreator 会被 redux 创建到 slice.actions 上)、extraReducers(接异步)、手动定义异步 actionCreator(createAsyncThunk)。怎么做:切片中需要向外暴露的三类内容要记牢:默认暴露 export default countSlice.reducer;分别暴露同步 actionCreator:export const {addNum, decNum} = countSlice.actions;分别暴露异步 actionCreator。易错点:不要默认导出整个 slice;同步 actionCreator 从 slice.actions 解构;异步 actionCreator 不是写在普通 reducer 里,而是用 createAsyncThunk 定义、extraReducers 接收。转写稿只展开到同步流程,异步按本节小结记流程即可,不要额外编造实现。
三、react-redux 接入:Provider 绑定 store
为什么需要:转写稿里先直接用 store.getState() 读、store.dispatch() 改,数据从 99 改到 117,但页面不更新。原因是函数组件没有 props 变化、没有 setState、也没有 forceUpdate,仓库变了不会自动触发重新渲染。怎么想:需要 react-redux 把仓库接入 React 渲染链路。怎么做:安装后导入 Provider,包裹根组件并绑定 store:
js
import { Provider } from 'react-redux'
import store from './store'
root.render(
<Provider store={store}>
<App />
</Provider>
)易错点:store 要正确导入;Provider 放在 BrowserRouter 内外都可以;接入后组件里不要再用 store.getState() 读数据。
四、useSelector:读取状态数据
为什么需要:组件要响应仓库变化。怎么想:useSelector 的参数回调会收到整个仓库 state,回调的返回值就是 useSelector 的返回值。怎么做:读整个仓库就 useSelector(state => state);读切片就 useSelector(state => state.goods);读具体值就 useSelector(state => state.goods.goods) 或解构 num。转写稿调试时先 console.log(state),确认拿到的是整个仓库对象。易错点:回调参数不是某个切片,而是整个 state;要写清楚 state.goods.goods,不要误写成 state.goods 就当成数组。
五、useDispatch:同步修改状态
为什么需要:组件里要触发 actionCreator。怎么想:useDispatch() 创建 dispatch 函数,功能等同于 store.dispatch。怎么做:const dispatch = useDispatch(); dispatch(addNum(3))。购物车案例中表单提交要 e.preventDefault(),用 useRef 取商品名和价格,价格转数字,再 dispatch(addGoods({ id: nanoId(), gname, price }))。转写稿调试时先打印 payload,确认对象正确;nanoId() 忘加括号会导致 ID 不对。易错点:actionCreator 要从 slice.actions 导入;payload 结构要和 reducer 里一致;商品列表用 goods.map 渲染时记得写 key。
六、案例串起来与异步小结
为什么需要:把页面和仓库连起来。怎么做:建 goodsSlice、cartSlice、store,Provider 包根组件;添加商品后 dispatch(addGoods(...)),再用 useNavigate 跳商品列表;列表页用 useSelector 取 state.goods.goods 渲染。易错点:切片名、store key、useSelector 读取路径三者要一致。异步按本节笔记记:extraReducers 是异步入口,异步 actionCreator 用 createAsyncThunk 手动定义,组件里 dispatch(异步actionCreator(payload))。
先理解问题:直接操作 store 为什么页面不更新
为什么需要:视频1先用最原始方式在 React 里读 Redux:store.getState() 能读到 counter.num,点击按钮 store.dispatch(addN(3)) 后仓库值从 99 变成 117,但页面不刷新。
怎么想:函数组件触发更新通常靠 props、state,类组件还可 forceUpdate;这里只改了仓库,三者都没发生,所以 App 没有重新渲染。
怎么做:引入 react-redux,让组件能方便地读仓库、拿 dispatch,并由它触发更新。
易错点:不要把“仓库数据变了”误认为“界面一定变了”;也不要在组件里反复直接 import store 后手动 getState。
jsx
// 原始写法示意:能改值,但不触发组件更新
const num = store.getState().counter.num
<button onClick={() => store.dispatch(addN(3))}>加3</button>Provider 绑定 store
为什么需要:react-redux 要让 App 及所有后代组件都能拿到同一个 store。
怎么想:用 Provider 包裹根组件,并把 store 赋给它的 store 属性。
怎么做:入口文件导入 Provider、store,包裹 App;BrowserRouter 放里放外都可以。
易错点:属性名是 store 不是 star;忘记包裹时,后代里 useSelector/useDispatch 会报错。
jsx
import React from 'react'
import ReactDOM from 'react-dom/client'
import { Provider } from 'react-redux'
import { BrowserRouter } from 'react-router-dom'
import App from './App'
import store from './store'
const root = ReactDOM.createRoot(document.querySelector('#root'))
root.render(
<Provider store={store}>
<BrowserRouter>
<App />
</BrowserRouter>
</Provider>
)useSelector 读取数据
为什么需要:不想在每个组件里直接操作 store.getState()。
怎么想:useSelector 接收回调,回调参数是整个仓库 state,返回值就是组件要用的数据。
怎么做:取 counter 切片:useSelector(state => state.counter.num);取整个切片:state => state.counter。
易错点:state 是整个仓库,不是某个切片;要取 state.goods.goods 这类层级时先看清切片结构。
jsx
import { useSelector } from 'react-redux'
const num = useSelector(state => state.counter.num)useDispatch 修改数据
为什么需要:修改仓库仍要派发 action。
怎么想:useDispatch() 创建 dispatch,功能等同 store.dispatch。
怎么做:导入 action creator,点击时 dispatch(addN(3))。
易错点:不要直接改 state;action creator 要从对应切片文件导入。
jsx
import { useDispatch } from 'react-redux'
import { addN } from './store/slice/counterSlice'
const dispatch = useDispatch()
<button onClick={() => dispatch(addN(3))}>加3</button>购物车练习:路由与静态页面
为什么需要:案例有添加商品、商品列表、购物车列表三个页面。
怎么想:用路由切换页面,先搭静态结构,再接仓库。
怎么做:配置 /addGoods、/goodsList、/cartList,根路径重定向到添加商品;App 里用 Link 导航、useRoutes 渲染。
易错点:忘记导入 Link;路由路径与 to 不一致。
jsx
const routes = [
{ path: '/addGoods', element: <AddGoods /> },
{ path: '/goodsList', element: <GoodsList /> },
{ path: '/cartList', element: <CartList /> },
{ path: '/', element: <Navigate to="/addGoods" /> }
]创建 goods、cart 切片与 store
为什么需要:商品和购物车数据要分模块管理。
怎么想:createSlice 建切片,configureStore 合并 reducer。
怎么做:goods 初始 { goods: [] },addGoods 先打印 payload;cart 同理先搭架子。
易错点:默认暴露 reducer,分别暴露 action creator;nanoid 要加括号。
js
import { createSlice } from '@reduxjs/toolkit'
import { nanoid } from 'nanoid'
const goodsSlice = createSlice({
name: 'goods',
initialState: { goods: [] },
reducers: {
addGoods(state, { payload }) {
state.goods = [...state.goods, { id: nanoid(), ...payload }]
}
}
})
export const { addGoods } = goodsSlice.actions
export default goodsSlice.reducerjs
import { configureStore } from '@reduxjs/toolkit'
import goods from './slice/goodsSlice'
import cart from './slice/cartSlice'
const store = configureStore({ reducer: { goods, cart } })
export default store添加商品并跳转
为什么需要:表单提交后要把商品写入仓库。
怎么想:useRef 取输入值,useDispatch 派发 addGoods,useNavigate 跳商品列表。
怎么做:e.preventDefault() 阻止默认提交;价格用 + 转数字;派发后跳 /goodsList。
易错点:忘记阻止默认行为会刷新;nanoid() 漏括号;action creator 路径别导错。
jsx
const handleSubmit = e => {
e.preventDefault()
const gname = gnameRef.current.value.trim()
const price = +priceRef.current.value.trim()
dispatch(addGoods({ gname, price }))
navigate('/goodsList')
}商品列表渲染
为什么需要:把仓库里的商品数组显示出来。
怎么想:useSelector 取 state.goods.goods,再 map 渲染。
怎么做:每项用 item.id 做 key,渲染 gname、price 和“加入购物车”按钮。
易错点:map 返回 JSX 要用圆括号;不要漏 key;切片层级别少写一层。
jsx
const { goods } = useSelector(state => state.goods)
return (
<ul>
{goods.map(item => (
<li key={item.id}>
<h3>{item.gname}</h3>
<p>{item.price}</p>
<button>加入购物车</button>
</li>
))}
</ul>
)这一节围绕“常见问题”展开:react-redux 接入后,为什么数据已经改了页面却不更新;以及购物车案例如何从静态页面推进到由仓库驱动。下面按知识点拆开。
常见问题:数据改了,页面为什么不动
为什么需要:直接 store.dispatch 能改仓库数据,但函数组件不会自动重新渲染。转写中点击“加 3”后,最新值已经变了,界面却没反应。
怎么想:函数组件触发更新的常见途径是 props 变化、state 变化或类组件 forceUpdate。只改仓库,不等于触发了这些途径。
怎么做:引入 react-redux,用 Provider 把 store 绑定到根组件,让组件通过 Hook 订阅仓库。
易错点:以为“数据变了页面就会变”;忘了 Provider store={store};把 store 当普通变量导入却未传给 Provider。
示意接入代码:
jsx
import { Provider } from 'react-redux'
import store from './store'
root.render(
<Provider store={store}>
<App />
</Provider>
)接入 react-redux:Provider、useSelector、useDispatch
为什么需要:Provider 让 App 及后代组件都能拿到仓库;useSelector 负责读,useDispatch 负责改。
怎么想:先安装 react-redux,再导入 Provider 包裹根组件并绑定 store;组件内不再直接导入 store.getState()。
怎么做:useSelector 的回调参数是整个仓库 state,返回值就是组件要用的数据;useDispatch 创建 dispatch,再配合 action creator 修改数据。
易错点:useSelector 里把 state 当成某个切片;忘记 dispatch(actionCreator(payload));组件不在 Provider 内。
jsx
import { useSelector, useDispatch } from 'react-redux'
const goods = useSelector(state => state.goods.goods)
const dispatch = useDispatch()
dispatch(addGoods(payload))调试时可先打印 state,确认拿到的到底是整个仓库还是切片数据。
购物车案例:先搭静态页面和路由
为什么需要:案例需要“添加商品 → 商品列表 → 购物车列表”的页面切换,先把页面和路由跑通,再接仓库。
怎么想:建 AddGoods、GoodsList、CartList 三个页面;配置路由表,用 BrowserRouter 包裹 App,用 Routes/Route 展示页面,用 Link 做导航。
怎么做:路由表里配置 addgoods、goodslist、cartlist,并加重定向;App 中用 useRoutes 渲染。
易错点:忘了导入 Link;BrowserRouter 没包住 App;路由路径和 to 不一致。
jsx
<BrowserRouter>
<App />
</BrowserRouter>创建 goodsSlice、cartSlice 和 store
为什么需要:添加商品、加入购物车都要改仓库,需要按模块建切片。
怎么想:goodsSlice 管商品数组,cartSlice 管购物车数组;每个切片默认导出 reducer,分别导出 action creator;store 用 configureStore 合并 reducer。
怎么做:商品切片初始 goods: [],addGoods 中把 payload 追加进数组,并用 nanoid() 生成 id。
易错点:name 与 store 中 reducer 的 key 混乱;忘了导出 action;state.goods.goods 写错层级。
js
const goodsSlice = createSlice({
name: 'goods',
initialState: { goods: [] },
reducers: {
addGoods(state, { payload }) {
state.goods.push({ id: nanoid(), ...payload })
}
}
})
export const { addGoods } = goodsSlice.actions
export default goodsSlice.reducer添加商品与商品列表渲染
为什么需要:表单提交后要把商品写入仓库,并跳转到商品列表;列表要从仓库取数据渲染。
怎么想:onSubmit 中先 preventDefault(),用 useRef 取输入值,价格转数字,组装 { gname, price },dispatch(addGoods(goods)) 后 navigate('/goodslist')。
怎么做:列表页用 useSelector(state => state.goods.goods) 取数组,map 渲染,key 用商品 id。
易错点:忘了阻止表单默认提交;价格仍是字符串;ref.current 未绑定;dispatch 没导入 action;列表 key 用索引或漏写。
jsx
const handleSubmit = e => {
e.preventDefault()
const gname = goodsNameRef.current.value.trim()
const price = +priceRef.current.value
dispatch(addGoods({ gname, price }))
navigate('/goodslist')
}调试时先打印 payload,再到 Redux 调试工具里看 goods 数组是否新增;最后检查列表是否正确 map 渲染。
课堂画面





代码示例
示例一:configureStore + createSlice + Provider / useSelector / useDispatch(同步与异步)
html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<title>Redux Toolkit + React-Redux 计数器</title>
<style>
body { font-family: sans-serif; }
button { margin-right: 8px; }
</style>
</head>
<body>
<div id="root"></div>
<script src="https://unpkg.com/react@18/umd/react.development.js"></script>
<script src="https://unpkg.com/react-dom@18/umd/react-dom.development.js"></script>
<script src="https://unpkg.com/@reduxjs/[email protected]/dist/redux-toolkit.umd.js"></script>
<script src="https://unpkg.com/[email protected]/dist/react-redux.umd.js"></script>
<script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
<script type="text/babel">
const { configureStore, createSlice, createAsyncThunk } = RTK;
const { Provider, useSelector, useDispatch } = ReactRedux;
/* 1. 手动定义异步的 actionCreator(必须写在 createSlice 之前) */
const asyncAddNum = createAsyncThunk('count/asyncAddNum', async (payload) => {
await new Promise((resolve) => setTimeout(resolve, 800));
return payload; // 返回值会出现在 action.payload 上
});
/* 2. 切片:name / initialState / reducers / extraReducers */
const countSlice = createSlice({
name: 'count',
initialState: { num: 99 },
reducers: { // 同步的程序员
addNum(state, action) {
state.num += action.payload;
},
decNum(state, action) {
state.num -= action.payload;
}
},
extraReducers: (builder) => { // 异步的程序员
builder.addCase(asyncAddNum.fulfilled, (state, action) => {
state.num += action.payload;
});
}
});
/* 3. 切片需要向外暴露的东西 */
const countReducer = countSlice.reducer; // 默认暴露
const { addNum, decNum } = countSlice.actions; // 分别暴露同步 actionCreator
// 异步 actionCreator 就是上面自己定义的 asyncAddNum
/* 4. store */
const store = configureStore({
reducer: {
count: countReducer
}
});
/* 5. 组件 */
function App() {
const num = useSelector((state) => state.count.num); // 读取状态数据
const dispatch = useDispatch(); // 创建 dispatch 函数
return (
<div>
<h3>num: {num}</h3>
<button onClick={() => dispatch(addNum(3))}>同步 +3</button>
<button onClick={() => dispatch(decNum(1))}>同步 -1</button>
<button onClick={() => dispatch(asyncAddNum(5))}>异步 +5</button>
</div>
);
}
/* 6. Provider 包裹根组件并绑定 store */
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
<Provider store={store}>
<App />
</Provider>
);
</script>
</body>
</html>要点与易错点:
configureStore({ reducer: { count: countReducer } })里的 键名count才是仓库里的 state 名字,createSlice的name只决定 action type 前缀(count/addNum),两者不要混为一谈;所以取值是state.count.num。reducers里可以直接state.num += action.payload,Redux Toolkit 内部用 Immer 帮你生成新 state,不要再手写return {...state},也不要“既改又 return”。- 同步的 actionCreator 会自动挂在
countSlice.actions上,export const { addNum, decNum } = countSlice.actions;reducer 用export default countSlice.reducer,别把actions当 reducer 传进 store。 createAsyncThunk的调用要放在createSlice之前:extraReducers的回调在createSlice执行时就会立即运行,此时asyncAddNum还没初始化会直接报 TDZ 错误。- 只有被
<Provider store={store}>包裹的组件才能用useSelector/useDispatch;useSelector的回调参数是整个仓库 state,回调返回值就是 hook 的返回值。 - 以前直接
store.dispatch(addNum(3))能改数据但不会触发组件重新渲染,这就是要引入 react-redux 的原因。 - 修改数据统一走
dispatch:同步dispatch(addNum(3)),异步dispatch(asyncAddNum(5))。
示例二:商品 / 购物车双切片(addGoods + nanoid + useSelector 渲染列表)
html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<title>商品 - 购物车</title>
<style>
body { font-family: sans-serif; }
.goods-item { border-bottom: 1px dashed #ccc; padding: 6px 0; }
.goods-item h3 { margin: 0 0 4px; }
</style>
</head>
<body>
<div id="root"></div>
<script src="https://unpkg.com/react@18/umd/react.development.js"></script>
<script src="https://unpkg.com/react-dom@18/umd/react-dom.development.js"></script>
<script src="https://unpkg.com/@reduxjs/[email protected]/dist/redux-toolkit.umd.js"></script>
<script src="https://unpkg.com/[email protected]/dist/react-redux.umd.js"></script>
<script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
<script type="text/babel">
const { configureStore, createSlice, nanoid } = RTK;
const { Provider, useSelector, useDispatch } = ReactRedux;
const { useRef } = React;
/* 1. 商品切片 */
const goodsSlice = createSlice({
name: 'goods',
initialState: { goods: [] },
reducers: {
addGoods(state, { payload }) {
state.goods = [...state.goods, { id: nanoid(), ...payload }];
}
}
});
const { addGoods } = goodsSlice.actions;
/* 2. 购物车切片 */
const carSlice = createSlice({
name: 'car',
initialState: { carList: [] },
reducers: {
addCar(state, { payload }) {
state.carList.push(payload);
}
}
});
const { addCar } = carSlice.actions;
/* 3. 仓库:两个切片合并 */
const store = configureStore({
reducer: {
goods: goodsSlice.reducer,
car: carSlice.reducer
}
});
/* 4. 页面 */
function App() {
const goodsNameRef = useRef();
const priceRef = useRef();
const dispatch = useDispatch();
const goods = useSelector((state) => state.goods.goods);
const carList = useSelector((state) => state.car.carList);
const handleSubmit = (e) => {
e.preventDefault(); // 阻止表单默认提交
const gname = goodsNameRef.current.value.trim();
const price = +priceRef.current.value; // 转成数字
const goodsObj = { gname, price };
dispatch(addGoods(goodsObj)); // 触发商品切片的 addGoods
goodsNameRef.current.value = '';
priceRef.current.value = '';
};
return (
<div>
<h3>添加商品的页面</h3>
<form onSubmit={handleSubmit}>
商品名称:<input type="text" ref={goodsNameRef} />
商品价格:<input type="text" ref={priceRef} />
<button>添加商品</button>
</form>
<h3>商品列表</h3>
<ul>
{goods.map((item) => (
<li key={item.id} className="goods-item">
<h3>{item.gname}</h3>
<p>价格:{item.price}</p>
<button onClick={() => dispatch(addCar(item))}>加入购物车</button>
</li>
))}
</ul>
<h3>购物车列表</h3>
<ul>
{carList.map((item, index) => (
<li key={index}>
{item.gname} —— 价格:{item.price}
</li>
))}
</ul>
</div>
);
}
/* 5. Provider 包裹根组件并绑定 store */
ReactDOM.createRoot(document.getElementById('root')).render(
<Provider store={store}>
<App />
</Provider>
);
</script>
</body>
</html>要点与易错点:
- 两个切片在
configureStore里用对象合并,读取时是state.goods.goods、state.car.carList:第一个goods是 store 的键名,第二个goods是切片里的 initialState 字段,写错就会得到undefined。 reducers里用{ payload }直接解构 action,省掉action.payload;dispatch(addGoods(goodsObj))传进去的对象就是payload。{ id: nanoid(), ...payload }的顺序不能反:写成{ ...payload, id: nanoid() }时,如果 payload 里也带id会覆盖生成的 id。- 表单用
onSubmit+e.preventDefault(),不写这一行会触发浏览器默认提交,页面刷新、仓库数据看起来“没生效”。 - 输入框取到的是字符串,价格要
+priceRef.current.value转成数字,否则后面做数值运算会变成字符串拼接。 - 列表渲染的
key要用item.id,不要用数组下标;goods.map用圆括号( )返回 JSX,用花括号{ }就必须写return。 - 加入购物车用
dispatch(addCar(item))把整条商品数据作为 payload 传给购物车切片;转写稿里addCar只打印了 payload,这里给了最简 push 实现,合并数量的逻辑等讲到功能时再补。 - 转写稿中“添加商品后跳到商品列表”用的是路由的编程式导航,这里是单文件演示,把添加页和列表页放在同一个组件里,效果一致。
常见错误
界面数据不刷新
- 现象:点按钮后
console.log(store.getState())数据确实变了,页面上的数字纹丝不动。 - 原因:只在模块里用
store.getState()/store.dispatch(),没有接到 React 的渲染链路上,组件不会重新渲染。 - 修法:组件内改用
useSelector读、useDispatch派发,把仓库更新接入 React 的更新机制。
- 现象:点按钮后
Provider 上下文找不到
- 现象:控制台报
could not find react-redux context value; please ensure the component is wrapped in a <Provider>。 - 原因:
<Provider>没包住报错的组件,或包了却漏传store。 - 修法:在
index.js用<Provider store={store}>包住<App />,与BrowserRouter谁在外层都行,但store属性不能漏。
- 现象:控制台报
actionCreator 是 undefined
- 现象:
dispatch(addGoods(obj))报addGoods is not a function。 - 原因:切片文件只
export default xxxSlice.reducer,忘了分别暴露actions;或 import 路径少写了一层目录。 - 修法:切片里补
export const { addGoods } = goodsSlice.actions,组件从 slice 文件按名导入,不要从store/index.js里拿。
- 现象:
reducer 返回值写错,state 被写坏
- 现象:添加一条商品后
state.goods变成数字或undefined,列表渲染直接报错。 - 原因:
return state.goods.push(item)——push返回的是新长度;或return state.goods把整个切片 state 换成了数组。 - 修法:要么
state.goods.push(item)后什么都不 return(Immer 允许直接改),要么return { ...state, goods: [...state.goods, item] },两种风格别混用。
- 现象:添加一条商品后
useSelector 返回新对象导致无限重渲染
- 现象:页面卡死,控制台刷
Maximum update depth exceeded。 - 原因:
useSelector(state => ({ list: state.car.carList }))每次返回新对象,引用永远不相等。 - 修法:一次只取一个字段;确实要取对象时给
useSelector传第二参shallowEqual。
- 现象:页面卡死,控制台刷
key / id 重复导致列表错乱
- 现象:增删商品后,某一行的输入内容或状态“跑”到别的行上。
- 原因:用数组
index或Date.now()当id和key。 - 修法:新增时用
nanoid()生成唯一id,渲染时key={item.id}。
表单提交整页刷新 / 取到 undefined
- 现象:点“添加商品”页面闪一下回到初始状态;或
goods对象的字段是undefined。 - 原因:处理函数没
e.preventDefault();ref 变量名与ref={}绑定名不一致;价格没转数字。 - 修法:函数首行
e.preventDefault();+priceRef.current.value或Number()转换后再组装对象。
- 现象:点“添加商品”页面闪一下回到初始状态;或
store 的 key 与 useSelector 取的对不上
- 现象:
useSelector(state => state.goods.goods)报Cannot read properties of undefined。 - 原因:
configureStore({ reducer: { goods, car } })里的 key 决定 state 结构,改名后组件没同步。 - 修法:先
useSelector(state => { console.log(state); return state })对一遍结构,再统一命名。
- 现象:
练习
打通“添加商品”全链路:ADD 页表单 →
dispatch(addGoods({ name, price }))→ 自动跳转商品列表页并渲染出新商品。要求价格入库为number,id用nanoid()生成。实现“加入购物车”:在 cartSlice 写
addCartreducer,规则为——购物车里已有该商品则count + 1,没有则追加{ id, name, price, count: 1 };点击后跳转购物车列表页。购物车数量加减:增加
incCount/decCount两个 reducer,数量减到 0 时把该商品从列表中移除。要求decCount里不出现state.carList = state.carList.filter(...)与push混用的写法。派生数据不进 store:在购物车页用
useSelector分别取出列表,在组件内计算总件数与总金额。验证:连续加购同一商品,总金额正确且页面不出现无限重渲染警告。异步加载商品:用
createAsyncThunk模拟请求(setTimeout+ Promise)拉取商品列表,在extraReducers中用builder.addCase分别处理pending / fulfilled / rejected,列表页展示 loading 与错误提示。
原视频对照
| # | 视频 | 时长 |
|---|---|---|
| 1 | react-redux基本使用 | 18:16 |
| 2 | 商品-购物车练习静态页面布局完成 | 09:09 |
| 3 | 创建商品切片-购物车切片及store | 06:52 |
| 4 | 添加商品完成 | 11:33 |
| 5 | 商品列表渲染完成 | 02:10 |
小结
react-redux只解决两件事:Provider把 store 注入 React 树,useSelector/useDispatch把读和写接进组件。- 完整链路是单向的:组件 dispatch → reducer 产出新 state →
useSelector订阅到变化 → 组件重渲染。少任何一环,页面就不动。 - 一个业务域一个 slice:
name决定 action type 前缀,initialState决定数据结构,reducers管同步,extraReducers管异步。 - 每个 slice 固定暴露三样:默认暴露
reducer、具名暴露actions、异步 thunk 单独具名暴露。漏一个就是运行时undefined。 configureStore中reducer的 key 就是 state 的顶层 key,命名必须与useSelector里的取值路径一致。- 总价、件数、过滤结果这类派生数据放在组件里算,不要存进 store,避免多份数据互相打架。
- Immer 让你能“直接改”,但要有意识地二选一:直接改并 return
undefined,或返回全新对象;两者混写是 state 被写坏的头号原因。 - 排错固定顺序:先
console.log(state)看结构对不对 → 再看 action 是否真的派发出去 → 最后看对应 reducer 是否命中。