Skip to content

常见问题(Day9 3/3) ​

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

学习目标 ​

  1. 能够说明 Redux 中 state、reducer、action creator 与 dispatch 的调用关系,并写出通过 dispatch(actionCreator(payload)) 触发 reducer 修改状态的完整流程。
  2. 能够实现“加入购物车”功能:根据商品 id 判断购物车中是否已存在,已存在则增加 buyNum,不存在则新增商品并初始化购买数量。
  3. 能够使用 useSelector 读取购物车切片数据,并完成购物车列表的商品名称、价格、数量等信息渲染。
  4. 能够实现购物车商品数量加减:点击加号增加数量;点击减号减少数量,当数量减至 0 时使用数组方法从购物车中删除该商品。
  5. 能够运用打印验证、逐步注释、缩小范围、阅读报错行号与搜索导入项等方法,定位并排除 Redux 项目中的常见运行错误。

知识点 ​

常见问题:Redux 操作与调试 ​

本节围绕 Redux 中“数据放在切片里、只能通过 reducer 改、组件里用 dispatch 触发、用 useSelector 读”的主线,梳理添加购物车、数量加减和调错时最容易乱的地方。

触发 reducer:为什么必须 dispatch ​

为什么需要:状态数据在 slice 的 initialState 里,组件不能直接改它;只有 reducer 里定义的方法能拿到 state 并修改。

怎么想:把 reducer 方法理解成“程序员”,action creator 理解成“产品经理”。组件不能直接找程序员,要先 dispatch 产品经理创建出的 action,再由 action 找到对应 reducer 执行。

怎么做:组件里先创建 dispatch,再导入 action creator,点击时调用:

js
const dispatch = useDispatch()

<button onClick={() => dispatch(addCart(good))}>
  加入购物车
</button>

易错点:addCart 不是 reducer 本身,而是 action creator;它执行后产生 action 对象,参数会进入 payload。调试时先在 reducer 里打印 payload,能执行到打印,说明 dispatch 和 action creator 这条链基本通了。

添加购物车:payload 里放什么 ​

为什么需要:加入购物车要把当前商品写入 cart 切片的 cartList,而只有 reducer 能改它,所以点击事件里必须 dispatch。

怎么想:先确认“我要把哪个商品放进数组”,所以 action creator 的参数应是当前商品对象;reducer 里通过 payload 拿到它。

怎么做:转写中的核心逻辑是给商品补上购买数量,再放入数组:

js
state.cartList.push({
  ...payload,
  buyNumber: 1
})

易错点:商品对象原本没有购买数量,直接放进去后面无法加减;要先补 buyNumber。还要注意 action creator 的导出名与导入名一致,否则点击没反应或报未定义。

重复商品:先判断再新增或累加 ​

为什么需要:同一个商品重复加入时,如果每次都 push,数组会出现多条相同商品记录,购物车数量就乱了。

怎么想:通过商品 id 判断 cartList 中是否已存在;存在就改数量,不存在才新增。

怎么做:用 findIndex 找索引:

js
const index = state.cartList.findIndex(
  car => car.id === payload.id
)

if (index === -1) {
  state.cartList.push({ ...payload, buyNumber: 1 })
} else {
  state.cartList[index].buyNumber += 1
}

易错点:findIndex 找不到返回 -1,不要写成 0;找到索引后修改的是 state.cartList[index].buyNumber,不是新建一条。

购物车列表:用 useSelector 读取并渲染 ​

为什么需要:购物车页面要展示仓库里的 cartList,不能直接读组件本地数据。

怎么想:先取 cart 切片对象,再取其中的 cartList,最后 map 渲染。

怎么做:

js
const cartList = useSelector(state => state.cart.cartList)

{cartList.map(car => (
  <li key={car.id}>
    {car.name} {car.price} {car.buyNumber}
  </li>
))}

易错点:useSelector 返回的是切片对象,别忘记继续取 cartList;map 时要有 key。

数量加减:找到对象再改,减到 0 要删除 ​

为什么需要:加减数量仍是修改仓库数据,必须写 reducer,再由组件 dispatch。

怎么想:加号传 id 即可;减号先算 buyNumber - 1,小于等于 0 时从数组删除,否则回写数量。

怎么做:

js
const index = state.cartList.findIndex(
  car => car.id === payload
)

if (index !== -1) {
  const num = state.cartList[index].buyNumber - 1
  if (num <= 0) {
    state.cartList.splice(index, 1)
  } else {
    state.cartList[index].buyNumber = num
  }
}

易错点:减号逻辑不能简单 -= 1,否则会出现 0 或负数;删除数组元素用 splice(index, 1),而删除前必须先用 findIndex 拿到索引。

调错思路:先定位项目和入口,再逐步缩小范围 ​

为什么需要:多个文件、多个项目同时开着时,容易改错项目;报错信息也可能指向配置而不是业务代码。

怎么想:先确认“我改的是不是当前运行项目”,再确认框架能否运行,最后从入口和组件逐步删减定位。

怎么做:可在 index.js 打印 console.log(123),刷新能输出说明项目和打包基本正常。然后暂时去掉 Provider、BrowserRouter 等,逐步加回;若加回后报错,就检查 store 是否传入、导入名是否写错。看不懂错误时,复制报错变量名用 Ctrl+F 查导入。

易错点:报错说 useRouter is not defined,先接受“确实没导入或导错”,不要犟;有行号就看行号。配置类错误常定位不到业务文件,只能靠删减代码缩小范围。

Redux 操作流程示意图

先理解 Redux 的数据流 ​

为什么需要:数据都放在切片(slice)的 initialState 里,组件不能随便改;只有 reducer 里定义的方法能拿到 state 并修改它。
怎么想:把 reducer 想成“程序员”,action creator 想成“产品经理”,dispatch 是“派任务”。组件不能直接找程序员,要先 dispatch 产品经理创建出的 action,才能触发对应 reducer。
怎么做:组件里用 useDispatch() 得到 dispatch,用 useSelector() 读切片数据;触发修改时写 dispatch(actionCreator(payload))。
易错点:不要直接调用 reducer;dispatch 里传的应是 action creator 的调用结果,不是 reducer 本身;从 slice 解构 action creator 时名字要一致。

加入购物车:从按钮到 reducer ​

为什么需要:点“加入购物车”要把商品写进 cart 切片的 cartList,而只有 reducer 能改它。
怎么想:按钮事件里一定先 dispatch,再让 action creator 生成 action,payload 传当前商品对象。
怎么做:按转写稿整理的关键逻辑如下,先打印 payload 验证是否传到 reducer:

js
const dispatch = useDispatch()

// 点击加入购物车
dispatch(ADD_CAR(good))

// cart slice 的 reducer 中
const index = state.cartList.findIndex(car => car.id === payload.id)
if (index === -1) {
  state.cartList = [...state.cartList, { ...payload, buyNumber: 1 }]
} else {
  state.cartList[index].buyNumber += 1
}

易错点:第一次写时容易直接往数组里新增,导致同一商品出现多条记录;应先按 id 用 findIndex 判断,存在就加数量,不存在才新增。别忘了把新增的 reducer 对应 action creator 暴露出去。

购物车列表渲染 ​

为什么需要:加入购物车成功后要跳到购物车页并展示仓库里的数据。
怎么想:列表数据来自 cart 切片的 cartList,读取用 useSelector,渲染用 map。
怎么做:转写稿中的结构是取 cartList,每项 key 用 car.id,显示名称、价格、购买数量:

js
const { cartList } = useSelector(state => state.cart)

cartList.map(car => (
  <li key={car.id}>
    {car.name} {car.price} {car.buyNumber}
  </li>
))

易错点:useSelector 返回的是切片对象,要再取 cartList;跳转购物车页要用编程式导航,路径为 cart,并确认导航 Hook 已导入。

数量加减与删除 ​

为什么需要:购物车要支持加号和减号,仍然是在修改仓库数据。
怎么想:加号只需要商品 id,用 find 或 findIndex 找到对象后 buyNumber += 1;减号要先算出减后的数量,小于等于 0 就删除,否则赋值。
怎么做:转写稿整理如下:

js
// 加
let car = state.cartList.find(car => car.id === payload)
car.buyNumber += 1

// 减
let index = state.cartList.findIndex(car => car.id === payload)
let num = state.cartList[index].buyNumber - 1
if (num <= 0) {
  state.cartList.splice(index, 1)
} else {
  state.cartList[index].buyNumber = num
}

易错点:删除必须拿索引,所以减号逻辑更适合 findIndex;splice 会改原数组;每新增一个 reducer,都要记得暴露对应 action creator,并在组件里 dispatch(DEC_BY_NUMBER(car.id))。

调错顺序 ​

为什么需要:多个文件、多个项目同时开着时,很容易改错项目或漏导入。
怎么想:先确认“是不是这个项目、框架能不能跑”,再逐步缩小范围。
怎么做:在 index.js 打 console.log(123),能输出说明项目和打包基本正常;再简化 Provider、BrowserRouter 等配置,逐步加回,看哪一步报错;有报错先看文件名、行号和变量名。转写稿里就出现过忘写 store、路由 Hook 没导入/拼写不对的问题。
易错点:不要看到报错还说“我已经导了”,应复制报错变量去 Ctrl+F 查;找不到文件级错误时,多半是配置不正确,就从入口和 App 开始删代码排除。

课堂画面 ​

课堂画面 1

课堂画面 2

课堂画面 3

课堂画面 4

课堂画面 5

代码示例 ​

示例一:cartSlice.js —— 购物车切片的三个 reducer(可直接 node cartSlice.js 运行验证) ​

js
// cartSlice.js
const { createSlice, configureStore } = require('@reduxjs/toolkit')

const cartSlice = createSlice({
  name: 'cart',
  // 数据都放在切片的 initialState 里
  initialState: { cartList: [] },
  reducers: {
    // 加入购物车:已存在就改数量,不存在才新增
    addCart(state, action) {
      const index = state.cartList.findIndex(car => car.id === action.payload.id)
      if (index === -1) {
        // 没找到,新增:展开商品对象,补一个购买数量
        state.cartList.push({ ...action.payload, buyNumber: 1 })
      } else {
        // 找到了,只改数量,不再往数组里塞新对象
        state.cartList[index].buyNumber += 1
      }
    },
    // 加号:payload 只传 id 就够了
    addByNumber(state, action) {
      const car = state.cartList.find(car => car.id === action.payload)
      if (car) car.buyNumber += 1
    },
    // 减号:要删除数组元素,必须拿到索引,只能用 findIndex
    decByNumber(state, action) {
      const index = state.cartList.findIndex(car => car.id === action.payload)
      if (index === -1) return
      const num = state.cartList[index].buyNumber - 1
      if (num <= 0) {
        state.cartList.splice(index, 1)   // 减到 0 就从购物车删掉
      } else {
        state.cartList[index].buyNumber = num
      }
    }
  }
})

// createSlice 会给每个 reducer 自动生成一个同名的 action creator
const { addCart, addByNumber, decByNumber } = cartSlice.actions

const store = configureStore({ reducer: { cart: cartSlice.reducer } })

store.dispatch(addCart({ id: 1, name: '小米手机', price: 1999 }))
store.dispatch(addCart({ id: 1, name: '小米手机', price: 1999 })) // 重复商品 → 数量 +1
store.dispatch(addCart({ id: 2, name: '华为手机', price: 2999 }))
store.dispatch(addByNumber(1))
store.dispatch(decByNumber(1))
store.dispatch(decByNumber(2)) // 华为减到 0 → 被删除
console.log(JSON.stringify(store.getState().cart.cartList))
// [{ "id":1, "name":"小米手机", "price":1999, "buyNumber":2 }]

关键行与易错点

  • state.cartList.findIndex(...) === -1 是「不存在」的判断依据,findIndex 找不到时返回 -1,不能用 find 的结果去 splice(find 返回对象,不是索引)。
  • push({ ...action.payload, buyNumber: 1 }):传进来的商品对象只有 id/name/price,没有购买数量,必须自己补 buyNumber。
  • reducer 里直接 state.cartList[index].buyNumber += 1 是合法的,Redux Toolkit 内部用 Immer,别写成 state.cartList = [...state.cartList] 再改。
  • decByNumber 必须先算出 num 再判断,不能先 buyNumber -= 1 再判断,否则删除逻辑会漏掉。
  • 每新增一个 reducer,一定要记得从 cartSlice.actions 里解构导出,否则组件里 dispatch 时是 undefined。

示例二:GoodsList.jsx + CartList.jsx —— 组件里 dispatch 与 useSelector ​

jsx
// GoodsList.jsx
import { useDispatch } from 'react-redux'
import { addCart } from './cartSlice'

const goods = [
  { id: 1, name: '小米手机', price: 1999 },
  { id: 2, name: '华为手机', price: 2999 }
]

export default function GoodsList() {
  const dispatch = useDispatch()

  return (
    <ul>
      {goods.map(good => (
        <li key={good.id}>
          {good.name} —— {good.price} 元
          <button onClick={() => dispatch(addCart(good))}>加入购物车</button>
        </li>
      ))}
    </ul>
  )
}
jsx
// CartList.jsx
import { useSelector, useDispatch } from 'react-redux'
import { addByNumber, decByNumber } from './cartSlice'

export default function CartList() {
  // 先取到 cart 这个切片,再解构出 cartList
  const { cartList } = useSelector(state => state.cart)
  const dispatch = useDispatch()

  return (
    <ul>
      {cartList.map(car => (
        <li key={car.id}>
          {car.name} —— {car.price} 元 × {car.buyNumber}
          <button onClick={() => dispatch(addByNumber(car.id))}>+</button>
          <button onClick={() => dispatch(decByNumber(car.id))}>-</button>
        </li>
      ))}
    </ul>
  )
}

关键行与易错点

  • dispatch(addCart(good)) 里的 addCart 是 actions 里的 action creator,不是 reducers 里的函数;它执行后才产出 { type, payload },传的参数就是 payload。
  • onClick={() => dispatch(addCart(good))} 必须包一层箭头函数,直接写 onClick={dispatch(addCart(good))} 会在渲染时就执行。
  • useSelector(state => state.cart) 返回的是切片对象,别忘了再取 .cartList。
  • 加减号只传 car.id,不要传整个对象;decByNumber 靠这个 id 去 findIndex。
  • 想验证链路是否打通:先在 addCart 里 console.log(action.payload),能打印出当前商品对象就说明 dispatch 没问题。

示例三:store.js + index.js —— 入口配置与调错定位 ​

js
// store.js
import { configureStore } from '@reduxjs/toolkit'
import cartReducer from './cartSlice'

export default configureStore({
  reducer: {
    cart: cartReducer   // key 就叫 cart,组件里 state.cart 才对得上
  }
})
jsx
// index.js
import React from 'react'
import ReactDOM from 'react-dom/client'
import { Provider } from 'react-redux'
import { BrowserRouter } from 'react-router-dom'
import store from './store'
import App from './App'

console.log(123) // 第一步:能打印出来,说明项目和打包都是正常的

ReactDOM.createRoot(document.getElementById('root')).render(
  <Provider store={store}>
    <BrowserRouter>
      <App />
    </BrowserRouter>
  </Provider>
)

关键行与易错点

  • <Provider store={store}> 的 store 属性忘写,页面会直接报错——这是最常见的「我明明导入了」类错误。
  • 调错顺序:先 console.log(123) 确认跑的是当前项目 → 再把 Provider、BrowserRouter 临时删掉看是否还报错 → 再清空 App 里的内容,逐个恢复,用排除法缩小范围。
  • 报错信息一定要看:xxx is not defined 说明没导入或名字拼错;有行号就按行号找,不要凭感觉。
  • store.js 里 reducer 的 key(cart)必须和组件里 state.cart 完全一致,改一个忘改另一个,取到的就是 undefined。

常见错误 ​

  • 点击“加入购物车”无反应,控制台也没有 reducer 打印 → 忘记 useDispatch、忘记导入 action creator,或导入名与 slice 导出名不一致 → 先补 const dispatch = useDispatch(),核对 import { addCart } from './cartSlice',在回调里先打印 dispatch(addCart(good)) 验证链路。
  • 把 action creator 当成 reducer 直接调用,或绕过 dispatch 直接改数据 → 混淆“同名导出”和“触发执行” → 组件事件里统一写 dispatch(actionCreator(payload)),数据修改只放在 reducer 中。
  • 同一商品多次加入后,购物车数组出现多条相同 ID 记录 → 新增前没有按 ID 判断商品是否已存在 → 用 findIndex 查 ID:存在则改 buyNumber,不存在才新增对象。
  • 数量减到 0 甚至负数,商品仍留在购物车 → 减操作只做 buyNumber -= 1,没有处理边界 → 先算新数量,<= 0 时用 splice(index, 1) 删除,否则再赋新数量。
  • 报错 xxx is not defined,但坚持“我已经导入了” → 导入路径、名称或作用域不对,未按报错标识符核对 → 复制报错标识符全局搜索,使用自动导入,核对文件与行号后再运行。
  • 改了代码怎么刷新都不生效 → 运行的不是当前项目/入口,或调试时被 Provider、Router 等配置错误干扰 → 先在 index.js 打印标记确认入口,再逐步注释 Provider/Router/App,缩小到具体报错文件。
  • useSelector 取到 undefined 或渲染报错 → 切片路径写错,如漏了 cart 或 cartList → 先打印 useSelector 的返回值,核对 state.cart.cartList 的实际结构再渲染。

练习 ​

  1. 给“加入购物车”按钮补全点击逻辑:使用 useDispatch,导入对应 action creator,传入当前商品对象,并写出验证 payload 是否正确的调试方式。
  2. 实现购物车 reducer 的“重复商品”逻辑:同一 ID 已存在时数量加 1,不存在时新增商品并设置 buyNumber: 1。
  3. 实现减号逻辑:数量大于 1 时减 1;数量等于 1 时再减则从购物车删除该商品。写出 reducer 分支和组件点击回调。
  4. 点击加号后 reducer 没有打印,控制台也无明显报错。列出你的排查顺序,要求从入口、导入、dispatch、action creator、Provider 五个方向检查。
  5. 用 useSelector 渲染购物车列表,要求 key 使用商品 ID,展示名称、价格、购买数量;若页面报 undefined,写出定位切片路径的步骤。

原视频对照 ​

#视频时长
1redux执行操作分析04:21
2添加购物车操作完成14:11
3购物车列表功能实现02:04
4购车商品数量操作12:15
5调错思路分析07:31

小结 ​

  • 组件层只负责读取数据和派发动作,数据变更入口要统一收敛到 reducer。
  • 点击无反应时先查 dispatch 链路,不要先怀疑样式或渲染。
  • 购物车数组操作先判存在性,再决定新增、更新还是删除。
  • 数量类字段必须显式处理 <= 0 边界,避免负数和脏数据。
  • 调错先确认运行入口和最小可运行状态,再逐步恢复代码定位。
  • 报错信息优先看标识符、文件和行号,先接受报错再验证导入。
  • action creator 与 reducer 同名不代表同一个东西,导入来源必须核对。
  • 数组方法熟练度直接影响 reducer 逻辑是否清晰、边界是否完整。