Skip to content

Redux 与状态管理(二) ​

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

学习目标 ​

  • 能够使用 RTK 的 createSlice、configureStore 创建并配置 store,定义商品列表、分类激活下标、购物车列表等状态及对应 reducer。
  • 能够编写异步 action,通过 axios 获取商品数据并存入 store,在组件中使用 useDispatch 触发 action、使用 useSelector 读取数据完成分类和商品列表渲染。
  • 能够实现分类点击激活交互与商品列表联动显示,使用 activeIndex 动态控制激活类名,并按当前分类下标进行条件渲染。
  • 能够实现添加购物车逻辑:根据商品 id 判断是否已存在,存在则 count + 1,不存在则追加新商品,并在商品项组件中提交完整商品对象。
  • 能够基于购物车列表计算商品总数量与总价,控制统计区域高亮及结算文案显示,并实现购物车列表渲染、数量增减和清空购物车功能。
  • 能够使用 useState 管理购物车显示隐藏的 UI 临时状态,区分组件本地状态与 Redux 全局状态,并在购物车为空时禁止打开购物车面板。

知识点 ​

Redux 是什么:把状态从组件里“提”出来 ​

Redux 是 React 最常用的集中状态管理工具,类似于 Vue 中的 Pinia(Vuex),并且可以独立于框架运行。它的作用一句话概括:通过集中管理的方式管理应用的状态。

Redux 集中状态管理示意图

为什么需要它?因为组件之间共享数据时,如果只靠 props 一层层往下传,层级一深就会非常绕;兄弟组件之间通信也没有天然通道。怎么想?把“多个组件都要用的数据”从组件内部搬到组件外部的一个统一仓库里,组件只负责读和改,不再互相传。怎么做?在项目里建立 store,把状态、修改状态的方法集中定义,再通过 Provider 注入到组件树。易错点:不是所有状态都该进 Redux。转写稿在“控制购物车显示隐藏”里专门强调:这个显示状态只在当前组件有效、用完即没,属于 UI 临时状态,用 useState 就够了,不必放进 store。

为什么要使用 Redux:三个理由 ​

  1. 独立于组件,无视组件之间的层级关系,简化通信问题;
  2. 单向数据流清晰,易于定位 bug;
  3. 调试工具配套良好,方便调试。

为什么需要这三点?因为中大型应用里,状态来源一多,最怕“不知道谁改了数据”。怎么想?Redux 把修改入口收窄:组件不能直接改 state,只能 dispatch 一个 action,由 reducer 统一处理。怎么做?转写稿的调试过程很直观:打开浏览器 Network,能看到接口请求;再打开 Redux 调试工具,能看到确实触发了 setFoodsList 这样的 action,右侧 state 里 foods 模块的数据被存了下来。易错点:不要绕过 dispatch 直接改 store 里的对象,否则调试工具里的 action 记录就失去意义。

用案例理解数据流:分类与商品列表渲染 ​

转写稿用美团案例演示:左侧分类列表、右侧商品列表都要从后端拿数据。为什么需要 Redux?因为这两块数据要被多个组件共享。怎么想?把数据放进 store 的 foods 模块,用异步 action 请求接口,组件只负责触发和渲染。怎么做?先启动 mock 服务(提供可请求的接口)和前端服务,再写 store:

js
import { createSlice } from '@reduxjs/toolkit'
import axios from 'axios'

const foodsStore = createSlice({
  name: 'foods',
  initialState: { foodsList: [] },
  reducers: {
    setFoodsList(state, action) {
      state.foodsList = action.payload
    }
  }
})

// 异步 action
const fetchFoodsList = () => {
  return async (dispatch) => {
    const res = await axios.get('接口地址')
    dispatch(setFoodsList(res.data))
  }
}

export const { setFoodsList } = foodsStore.actions
export default foodsStore.reducer
export { fetchFoodsList }

然后在入口组合:

js
import { configureStore } from '@reduxjs/toolkit'
import foodsReducer from './modules/foods'

const store = configureStore({
  reducer: { foods: foodsReducer }
})
export default store

再在 index.js 里用 react-redux 的 Provider 包住 App,并传入 store。

易错点:转写稿里口述的“create.Slice”“configure store”“exos”分别是 createSlice、configureStore、axios;异步函数必须 return 一个函数并接收 dispatch,否则 thunk 不生效。

组件里怎么用:useDispatch 与 useSelector ​

为什么需要两个 Hook?因为组件要“发动作”和“读数据”。怎么想?useDispatch 拿到 dispatch 方法,useSelector 从 store 里映射出需要的字段。怎么做?在根组件里触发一次请求:

js
const dispatch = useDispatch()
useEffect(() => {
  dispatch(fetchFoodsList())
}, [dispatch])

渲染时:

js
const { foodsList } = useSelector(state => state.foods)

易错点:useSelector 里的 state.foods 必须和 configureStore 里注册的模块名一致;转写稿里反复提醒“得跟 store 里配置的模块名字匹配上”。

增删改查里的典型写法与调试 ​

添加购物车时,核心判断是“当前商品是否已添加过”。怎么想?用 find 按 id 查找,找到就 count 加一,找不到就 push。怎么做?

js
addCart(state, action) {
  const item = state.cartList.find(i => i.id === action.payload.id)
  if (item) item.count++
  else state.cartList.push(action.payload)
}

统计总价时用 reduce 累加“单价 × 数量”,再用 toFixed(2) 保留两位小数;购物车数量为 0 时不给高亮类名。易错点:转写稿里减号逻辑一开始会减成负数,后来补了判断——count 已经等于 0 就直接 return;另外 Redux 里没有 Vue 那种“计算属性”,每次数据变化组件重新渲染时会重新计算,所以直接在组件里算即可。

从“列表渲染”理解 RTK 数据流 ​

为什么需要:分类和商品列表都来自后端,不能继续用静态模拟数据;需要把异步请求结果放进 store,再由组件订阅渲染。
怎么想:数据属于跨组件共享状态,用 RTK 管理;请求是异步操作,所以先写同步 reducer,再写异步 action。
怎么做:在 foods 子模块里定义 list,同步 setFoodsList 负责写入;异步 fetchFoodsList 内用 axios.get 取数据,再 dispatch(setFoodsList(data))。入口用 configureStore 组合 reducer,并用 Provider 注入。组件里用 useDispatch 触发、useEffect 只请求一次,用 useSelector 取 state.foods.list 渲染。
易错点:createSlice 的模块名要和 useSelector 里的字段匹配;异步函数要返回函数并接收 dispatch;请求地址来自 mock 服务启动后给出的接口地址,不要写错。

js
const foodsSlice = createSlice({
  name: 'foods',
  initialState: { list: [] },
  reducers: {
    setFoodsList(state, action) { state.list = action.payload }
  }
})

const fetchFoodsList = () => {
  return async (dispatch) => {
    const res = await axios.get('接口地址')
    dispatch(setFoodsList(res.data))
  }
}

调试时看 Network 是否发出请求,再看 Redux DevTools 是否出现 setFoodsList,确认数据进 store 后再渲染。

分类激活与右侧列表切换 ​

为什么需要:点击左侧分类要切换激活态,同时右侧只显示对应商品列表。
怎么想:这是典型 tab 切换:先记录当前项下标 activeIndex,再用它和当前 index 比较,决定类名和内容是否显示。
怎么做:在 store 新增 activeIndex 和 changeActiveIndex;菜单项点击时 dispatch(changeActiveIndex(index));类名用 activeIndex === index && 'active';右侧列表同样用 activeIndex === index 做条件渲染。
易错点:不要只改样式不存下标;activeIndex 要取 store 中的值,不能误用局部变量。

js
onClick={() => dispatch(changeActiveIndex(index))}
className={activeIndex === index ? 'active' : ''}

转写中的调试过程是:默认 activeIndex 为 0,点第二项变 1,点第三项变 2,说明下标记录成功;再加条件后,右侧随点击切换。

添加购物车与统计区域 ​

为什么需要:点击商品加号要把商品加入 cartList,并统计数量、总价和高亮状态。
怎么想:先判断购物车中是否已有该商品:用 find 按 id 查找;找到则 count + 1,找不到则 push 新项。数量用 cartList.length,总价用 reduce 累加“单价 × 数量”。
怎么做:新增 addCart reducer;商品项组件点击加号时提交完整商品对象。统计组件用 useSelector 取 cartList,计算 totalPrice;cartList.length > 0 时显示高亮和结算文案。
易错点:find 找不到返回 undefined,判断要写清楚;总价初始值给 0;Redux 没有 Vue 那种计算属性,但组件重新渲染时会重新计算。

js
addCart(state, action) {
  const item = state.cartList.find(i => i.id === action.payload.id)
  if (item) item.count += 1
  else state.cartList.push(action.payload)
}

调试:点同一商品加号,count 从 1 变 2,不新增项;点另一商品才新增一项。

购物车增减、清除与边界 ​

为什么需要:购物车列表要能增减数量、清空列表。
怎么想:增减都要先按 id 找到当前项;减少到 0 时不能再减成负数。清空直接把 cartList 置空数组。
怎么做:新增 incrementCount、decrementCount、clearCart;减到 0 时直接 return。组件中加号、减号分别 dispatch,清空按钮提交 clearCart。
易错点:减数量忘记边界判断;传参要传对象且包含 id;清空后统计区域应恢复灰色。

js
decrementCount(state, action) {
  const item = state.cartList.find(i => i.id === action.payload.id)
  if (!item || item.count <= 0) return
  item.count -= 1
}

控制购物车显示隐藏 ​

为什么需要:点击统计区域弹出购物车和蒙层,点击蒙层收起。
怎么想:这是只在当前组件使用的 UI 临时状态,不必放 Redux,用 useState 即可。
怎么做:声明 visible;统计区域点击时,只有 cartList.length > 0 才 setVisible(true);蒙层点击设 false;蒙层和购物车列表用 visible 控制类名。
易错点:没有购物车数据时不应弹出空列表;不要把所有状态都塞进 store,是否共享是判断标准。

js
const [visible, setVisible] = useState(false)
const openCart = () => {
  if (cartList.length > 0) setVisible(true)
}

至此,分类、商品列表、加购、统计、购物车操作和显隐交互形成完整闭环。

本节目标与效果 ​

本节做一个最小可运行的计数器:页面上有“-”“数量”“+”三个元素,点击按钮后数量变化。需求限定为:不和任何框架绑定、不使用构建工具,只通过 CDN 引入 Redux,用纯 Redux 管理状态。

纯Redux计数器实现效果

转写稿主要讲的是美团案例中的 RTK、异步 action、购物车等内容,与本节“纯 Redux 计数器”不是同一个需求,因此不能把 RTK、axios、thunk 等代码硬套进来。这里只借用转写稿反复强调的调试顺序:先看 action 是否触发,再看 store 状态是否更新,最后看视图是否重新渲染。转写稿中出现的“dipatch”应写作 dispatch,“reucer”应写作 reducer,本节代码里也要避免这类拼写错误。

步骤一:定义 reducer 函数 ​

为什么需要:Redux 不允许视图随便改状态,必须有一个“根据 action 计算新状态”的纯函数,这就是 reducer。
怎么想:计数器状态可以设计成 { count: 0 };收到 INCREMENT 返回 count + 1,收到 DECREMENT 返回 count - 1,不认识的动作返回原状态。
怎么做:写 counterReducer(state = { count: 0 }, action),用 switch(action.type) 分支返回新对象。
易错点:不要写 state.count++ 直接改原对象;必须返回新对象。default 必须 return state,否则首次初始化或未知 action 会返回 undefined。

步骤二:使用 createStore 创建 store ​

为什么需要:reducer 只是规则,store 才是真正保存状态、接收 action、通知订阅者的实例。
怎么想:把 reducer 交给 Redux.createStore,Redux 内部会先调用一次 reducer 得到初始状态。
怎么做:const store = Redux.createStore(counterReducer)。
易错点:Redux 来自 CDN 脚本,必须保证 redux.min.js 在业务 <script> 之前加载;否则会报 Redux is not defined。纯 Redux 没有 configureStore,不要混入 RTK 写法。

步骤三:订阅数据变化并更新视图 ​

为什么需要:Redux 状态变化不会自动改 DOM,需要 subscribe 在每次 dispatch 后得到通知。
怎么想:订阅回调里用 getState() 取最新状态,再写入 #count 的 innerText。
怎么做:store.subscribe(() => { console.log(store.getState()); document.getElementById('count').innerText = store.getState().count })。
易错点:只 dispatch 不 subscribe,控制台可能看到状态变了但页面不动;subscribe 回调不会自动传新状态参数,仍要调用 getState()。

步骤四:用 dispatch 提交 action ​

为什么需要:视图不能直接改 store,只能“告诉” Redux 想做什么。
怎么想:点击“+”提交 { type: 'INCREMENT' },点击“-”提交 { type: 'DECREMENT' }。
怎么做:给两个按钮绑定 click,在回调里 store.dispatch({ type: 'INCREMENT' }) 或 DECREMENT。
易错点:action 必须是对象且带 type;type 字符串要和 reducer 的 case 完全一致。写成 increment 小写或漏写 type,状态不会按预期变化。

步骤五:用 getState 获取最新状态 ​

为什么需要:更新视图、调试、后续计算都依赖当前最新状态。
怎么想:getState() 返回当前完整状态对象,本节是 { count: n },所以取 .count。
怎么做:在订阅回调中读取并渲染;调试时在控制台点按钮后观察 store.getState() 输出。
易错点:不要缓存旧状态对象后假设它会自动变;每次变化后重新 getState()。若页面数字不变,依次检查:按钮事件是否触发、dispatch 的 type 是否匹配、reducer 是否返回新对象、subscribe 是否更新 DOM。

完整代码 ​

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>

课件图示 ​

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

课件第36页:需求理解 点击+号添加当前商品到购物车列表

课件第37页:实现步骤 使用RTK管理新状态cartList 组件中点击时收集数据提交acti

课件第45页:需求劣迹 1. 点击统计区域时,购物车列表显示 2. 点击蒙层区域时,购物车和蒙

课件第46页:实现步骤 点击统计区域设置状态为true 使用useState声明控制显隐的状态

课件第31页:理解需求 Tab切换类交互 记录当前点击项 (activeIndex) 动态控制

课件第32页:步骤分析 使用RTK编写管理activeIndex 组件中点击时触发action

课件第39页:需求理解 1. 购物车数量和总价统计 2. 高亮功能实现

代码示例 ​

示例 1:纯 Redux 实现计数器(不依赖任何框架与构建工具) ​

html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8" />
  <title>Redux 快速体验 - 计数器</title>
  <style>
    body { font-family: system-ui, sans-serif; display: flex; gap: 12px; align-items: center; padding: 40px; }
    button { width: 40px; height: 40px; font-size: 20px; cursor: pointer; }
    #count { min-width: 40px; text-align: center; font-size: 24px; }
  </style>
</head>
<body>
  <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>
    // 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 对象,告诉 reducer 你想怎么改数据
    const inBtn = document.getElementById('increment')
    inBtn.addEventListener('click', () => {
      store.dispatch({ type: 'INCREMENT' })
    })

    // 5. 减
    const dBtn = document.getElementById('decrement')
    dBtn.addEventListener('click', () => {
      store.dispatch({ type: 'DECREMENT' })
    })
  </script>
</body>
</html>

关键行与易错点

  • createStore(counterReducer):创建 store 实例时,Redux 会立刻用 undefined 调用一次 reducer,所以 state = { count: 0 } 的默认值是初始化状态的唯一来源。
  • switch (action.type) 的 default: return state:必须原样返回旧的 state。漏写或返回 undefined,store 会报错/状态被清空。
  • reducer 里只能返回新对象(return { count: state.count + 1 }),不能 state.count++ 直接改原对象,否则订阅者拿到的还是同一引用,视图不更新。
  • subscribe 回调不会立即执行,只在 dispatch 之后触发;页面首次显示的数字来自 HTML 里写死的 0。
  • dispatch 的参数必须是带 type 属性的普通对象,type 写错(大小写不一致)会走到 default 分支,表现为“点了没反应”。

示例 2:按“五步法”拆解 + 首屏手动渲染 ​

html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8" />
  <title>Redux 五步法拆解</title>
  <style>
    body { font-family: system-ui, sans-serif; padding: 40px; }
    button { width: 40px; height: 40px; font-size: 20px; cursor: pointer; }
    #count { display: inline-block; min-width: 40px; text-align: center; font-size: 24px; }
    #log { color: #888; }
  </style>
</head>
<body>
  <button id="decrement">-</button>
  <span id="count">0</span>
  <button id="increment">+</button>
  <p id="log"></p>

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

  <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
      }
    }

    // 步骤 2:createStore 传入 reducer,生成 store 实例
    const store = Redux.createStore(counterReducer)

    const countEl = document.getElementById('count')
    const logEl = document.getElementById('log')

    // 步骤 5 的前置动作:先拿一次初始状态,把首屏渲染出来
    countEl.innerText = store.getState().count
    logEl.innerText = '当前 state:' + JSON.stringify(store.getState())

    // 步骤 3:subscribe 订阅数据的变化
    store.subscribe(() => {
      const state = store.getState()          // 取最新状态
      countEl.innerText = state.count         // 更新到视图
      logEl.innerText = '当前 state:' + JSON.stringify(state)
    })

    // 步骤 4:dispatch 提交 action 对象,触发数据变化
    document.getElementById('increment').addEventListener('click', () => {
      store.dispatch({ type: 'INCREMENT' })
    })

    document.getElementById('decrement').addEventListener('click', () => {
      store.dispatch({ type: 'DECREMENT' })
    })
  </script>
</body>
</html>

关键行与易错点

  • store.getState() 是唯一读取状态的入口,不要在 reducer 外部直接改 store 里的对象;拿到的是引用,改它等于绕过 reducer。
  • subscribe 只在 dispatch 后触发,所以首屏数据要单独 getState() 渲染一次,这是新手最常见的“页面一开始是 0、但状态其实已经不是 0”的 bug 来源。
  • 每次 dispatch 都会通知所有订阅者,示例里只订阅了一次;如果给每个按钮都写一遍 subscribe,会出现重复渲染。
  • JSON.stringify(state) 便于在页面上观察状态流转,注意 state 里若出现循环引用会抛错,实际调试建议直接用 Redux DevTools。
  • 三个方法的分工要记牢:dispatch 提交 action → reducer 计算新 state → subscribe 收到通知 → getState 取值更新视图,这条单向数据流是后面 RTK 写异步 action(createAsyncThunk + dispatch)的基础。

常见错误 ​

1. 点击加号后购物车出现重复商品行

  • 现象:同一商品点两次加号,cartList 里出现两条记录,各自 count: 1
  • 原因:reducer 里直接 push,没有先判断当前商品是否已在列表中
  • 修法:先 state.cartList.find(item => item.id === action.payload.id),命中则 count + 1,未命中才 push;find 返回 undefined 即为判断依据

2. 减号一路减成负数

  • 现象:数量可以减到 -1、-2,界面显示异常
  • 原因:只写了 item.count -= 1,没有做下界保护
  • 修法:在 reducer 中前置判断 if (item.count <= 0) return,把"到 0 就不动"这条规则放在数据层而不是视图层(视图层禁用按钮是第二道防线,不是唯一防线)

3. dispatch 在异步 action 里报 dispatch is not a function

  • 现象:fetchFoodList 内部调用 dispatch 直接抛错,或 action 根本没发出去
  • 原因:return function () {...} 时漏写了形参 dispatch,或者把 thunk 写成了普通对象返回
  • 修法:严格写成 return (dispatch) => { ... };同时确认 configureStore 默认已集成 thunk,不需要手动加中间件

4. useSelector 取到 undefined,页面白屏或渲染报错

  • 现象:state.food.foodList 是 undefined,.map 直接崩
  • 原因:configureStore 里注册的 key(如 food)与 useSelector 中写的模块名不一致,或 slice 的 name 与注册 key 混用
  • 修法:以 configureStore({ reducer: { food: foodReducer } }) 中的 key 为唯一路径来源;给初始值兜底 state.food.foodList ?? []

5. 每次刷新页面都发两次接口请求

  • 现象:Network 面板里 fetchFoodList 出现两次
  • 原因:useEffect 没传依赖数组,任何 state 变化都重新执行
  • 修法:useEffect(() => { dispatch(fetchFoodList()) }, [dispatch]),把 dispatch 作为依赖而不是留空

6. 把购物车弹层的显示隐藏塞进了 store

  • 现象:一个只服务于单个组件的开关状态,却在 slice 里新增了 visible 字段和配套 reducer
  • 原因:没区分"跨组件共享状态"和"UI 临时状态"
  • 修法:只在本组件内用 useState;判断标准是——这个状态是否被两个及以上组件读写

练习 ​

练习 1|边界与上限 在现有 incrementCount / decrementCount 基础上,给数量加上"上限 99、下限 0"的双向保护。要求:保护逻辑写在 reducer 里,同时在 Count 组件上同步做按钮禁用,并说明为什么两处都要做。

练习 2|从 index 迁移到 id 当前菜单激活用的是 activeIndex(下标)。请改造成用分类 id 记录激活项,并回答:切换后,商品列表的条件渲染需要改动哪些地方?如果后端返回的分类顺序发生变化,两种方案分别会怎样?

练习 3|新增删除单项 在购物车列表中为每一项加一个删除按钮,点击后从 cartList 中移除该项。要求:新增独立 reducer(不要在 clearCart 里塞参数),组件中通过 dispatch 提交,并在 Redux DevTools 中确认 action 名称与 payload 结构正确。

练习 4|抽离总价计算 把统计区的 reduce 总价计算从组件中抽出来,封装成独立的 selector 函数(如 selectTotalPrice)。要求:说明抽离后组件内还剩哪些代码,以及当 cartList 变化时该计算是否仍会自动更新、为什么。

练习 5|排障演练 故意在 foodSlice 的 setFoodList 中把 state.foodList = action.payload 改成 return { ...state, list: action.payload }。运行后用 Redux DevTools 与 useSelector 分别观察,写出你看到的现象、定位过程和最终结论。

原视频对照 ​

#视频时长
1美团案例-分类和商品列表渲染17:22
2美团案例-点击分类激活交互实现07:29
3美团案例-商品列表切换显示02:47
4美团案例-添加购物车实现08:20
5美团案例-统计区域功能实现07:44
6美团案例-购物车列表功能实现12:10
7美团案例-控制购物车显示和隐藏07:09

小结 ​

  1. 这套案例的所有 reducer 都在回答同一个问题:"找到目标项 → 决定改还是不改 → 改多少",find + 条件判断是最常复用的骨架。
  2. 业务规则(不能减到负数、不能重复加购)必须落在 reducer 层,视图层的禁用/隐藏只是体验补充,不是数据正确性的保障。
  3. 判断一个状态该不该进 store,只看一件事:是否跨组件共享。购物车弹层开关是典型的 useState 场景。
  4. configureStore 中注册的 key 就是 useSelector 的访问路径,这两处是强耦合的,改名必须同步改。
  5. Redux 没有"计算属性",但也不需要——组件重渲染时会用最新 state 重新计算,所以把派生值写成纯函数(selector)反而更清晰、更易测。
  6. 异步数据流固定为三段式:组件 useEffect 触发 → thunk 内 dispatch 同步 action → reducer 落地 state,任何一段断了都表现为"数据没到"。
  7. 调试顺序建议固定为:先看 Network 有没有请求 → 再看 DevTools 有没有 action → 最后看 useSelector 取到的值,能避免大部分盲目改代码。
  8. 这个案例的完整链路(列表渲染 → 类名切换 → 条件渲染 → 增删改 → 显示隐藏)本质上都是同一个"数据驱动视图"模式的重复应用,掌握一次即可迁移到任意业务模块。