Skip to content

reduxjs/toolkit 小结与附录(Day9 2/3) ​

本页对应 React + TypeScript + 医通项目,共 5 段视频,约 48 分钟。

学习目标 ​

  1. 能说明仅使用 store.dispatch 修改仓库数据后 React 界面不更新的原因,并完成 react-redux 的安装、Provider 包裹根组件及绑定 store。
  2. 能使用 useSelector 读取整个仓库、指定切片及切片字段,并使用 useDispatch 创建 dispatch 函数触发 action creator 修改状态。
  3. 能使用 BrowserRouter、Routes、Route、Navigate 和 Link 配置添加商品、商品列表、购物车列表路由及默认重定向,完成静态页面布局与导航切换。
  4. 能按模块化目录创建 goodsSlice、cartSlice 和 store:定义 name、initialState、reducers,默认导出 reducer、分别导出 action creator,并用 configureStore 注册 reducer。
  5. 能在添加商品表单中阻止默认提交、通过 ref 获取商品名和价格、构造 payload,dispatch 商品切片的添加 action,生成 id 并追加到 goods 数组,提交后编程式跳转到商品列表。
  6. 能在商品列表页使用 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.reducer
js
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 渲染。

课堂画面 ​

课堂画面 1

课堂画面 2

课堂画面 3

课堂画面 4

课堂画面 5

代码示例 ​

示例一: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 实现,合并数量的逻辑等讲到功能时再补。
  • 转写稿中“添加商品后跳到商品列表”用的是路由的编程式导航,这里是单文件演示,把添加页和列表页放在同一个组件里,效果一致。

常见错误 ​

  1. 界面数据不刷新

    • 现象:点按钮后 console.log(store.getState()) 数据确实变了,页面上的数字纹丝不动。
    • 原因:只在模块里用 store.getState() / store.dispatch(),没有接到 React 的渲染链路上,组件不会重新渲染。
    • 修法:组件内改用 useSelector 读、useDispatch 派发,把仓库更新接入 React 的更新机制。
  2. 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 属性不能漏。
  3. 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 里拿。
  4. 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] },两种风格别混用。
  5. useSelector 返回新对象导致无限重渲染

    • 现象:页面卡死,控制台刷 Maximum update depth exceeded。
    • 原因:useSelector(state => ({ list: state.car.carList })) 每次返回新对象,引用永远不相等。
    • 修法:一次只取一个字段;确实要取对象时给 useSelector 传第二参 shallowEqual。
  6. key / id 重复导致列表错乱

    • 现象:增删商品后,某一行的输入内容或状态“跑”到别的行上。
    • 原因:用数组 index 或 Date.now() 当 id 和 key。
    • 修法:新增时用 nanoid() 生成唯一 id,渲染时 key={item.id}。
  7. 表单提交整页刷新 / 取到 undefined

    • 现象:点“添加商品”页面闪一下回到初始状态;或 goods 对象的字段是 undefined。
    • 原因:处理函数没 e.preventDefault();ref 变量名与 ref={} 绑定名不一致;价格没转数字。
    • 修法:函数首行 e.preventDefault();+priceRef.current.value 或 Number() 转换后再组装对象。
  8. 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 }) 对一遍结构,再统一命名。

练习 ​

  1. 打通“添加商品”全链路:ADD 页表单 → dispatch(addGoods({ name, price })) → 自动跳转商品列表页并渲染出新商品。要求价格入库为 number,id 用 nanoid() 生成。

  2. 实现“加入购物车”:在 cartSlice 写 addCart reducer,规则为——购物车里已有该商品则 count + 1,没有则追加 { id, name, price, count: 1 };点击后跳转购物车列表页。

  3. 购物车数量加减:增加 incCount / decCount 两个 reducer,数量减到 0 时把该商品从列表中移除。要求 decCount 里不出现 state.carList = state.carList.filter(...) 与 push 混用的写法。

  4. 派生数据不进 store:在购物车页用 useSelector 分别取出列表,在组件内计算总件数与总金额。验证:连续加购同一商品,总金额正确且页面不出现无限重渲染警告。

  5. 异步加载商品:用 createAsyncThunk 模拟请求(setTimeout + Promise)拉取商品列表,在 extraReducers 中用 builder.addCase 分别处理 pending / fulfilled / rejected,列表页展示 loading 与错误提示。

原视频对照 ​

#视频时长
1react-redux基本使用18:16
2商品-购物车练习静态页面布局完成09:09
3创建商品切片-购物车切片及store06: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 是否命中。