Redux介绍
Redux 是React最常用的集中状态管理工具,类似于Vue中的Pinia(Vuex),可以独立于框架运行
作用:通过集中管理的方式管理应用的状态

为什么要使用Redux?
- 独立于组件,无视组件之间的层级关系,简化通信问题
- 单项数据流清晰,易于定位bug
- 调试工具配套良好,方便调试
Redux快速体验
1. 实现计数器
需求:不和任何框架绑定,不使用任何构建工具,使用纯Redux实现计数器

使用步骤:
- 定义一个 reducer 函数 (根据当前想要做的修改返回一个新的状态)
- 使用createStore方法传入 reducer函数 生成一个store实例对象
- 使用store实例的 subscribe方法 订阅数据的变化(数据一旦变化,可以得到通知)
- 使用store实例的 dispatch方法提交action对象 触发数据变化(告诉reducer你想怎么改数据)
- 使用store实例的 getState方法 获取最新的状态数据更新到视图中
代码实现:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43
| <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> 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 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>
|
2. Redux数据流架构
Redux的难点是理解它对于数据修改的规则, 下图动态展示了在整个数据的修改中,数据的流向

为了职责清晰,Redux代码被分为三个核心的概念,我们学redux,其实就是学这三个核心概念之间的配合,三个概念分别是:
- state: 一个对象 存放着我们管理的数据
- action: 一个对象 用来描述你想怎么改数据
- reducer: 一个函数 根据action的描述更新state
Redux与React - 环境准备
Redux虽然是一个框架无关可以独立运行的插件,但是社区通常还是把它与React绑定在一起使用,以一个计数器案例体验一下Redux + React 的基础使用
1. 配套工具
在React中使用redux,官方要求安装俩个其他插件 - Redux Toolkit 和 react-redux
Redux Toolkit(RTK)- 官方推荐编写Redux逻辑的方式,是一套工具的集合集,简化书写方式
react-redux - 用来 链接 Redux 和 React组件 的中间件

2. 配置基础环境
使用 CRA 快速创建 React 项目
1
| npx create-react-app react-redux
|
安装配套工具
1
| npm i @reduxjs/toolkit react-redux
|
启动项目
3. store目录结构设计

通常集中状态管理的部分都会单独创建一个单独的 store
目录
应用通常会有很多个子store模块,所以创建一个 modules
目录,在内部编写业务分类的子store
store中的入口文件 index.js 的作用是组合modules中所有的子模块,并导出store
Redux与React - 实现counter
1. 整体路径熟悉

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28
| import { createSlice } from '@reduxjs/toolkit'
const counterStore = createSlice({ name: 'counter', initialState: { count: 1 }, reducers: { increment (state) { state.count++ }, decrement(state){ state.count-- } } })
const { increment,decrement } = counter.actions
const counterReducer = counterStore.reducer
export { increment, decrement } export default counterReducer
|
1 2 3 4 5 6 7 8 9 10
| import { configureStore } from '@reduxjs/toolkit'
import counterReducer from './modules/counterStore'
export default configureStore({ reducer: { counter: counterReducer } })
|
3. 为React注入store
react-redux负责把Redux和React 链接 起来,内置 Provider组件 通过 store 参数把创建好的store实例注入到应用中,链接正式建立
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| import React from 'react' import ReactDOM from 'react-dom/client' import App from './App'
import store from './store'
import { Provider } from 'react-redux'
ReactDOM.createRoot(document.getElementById('root')).render( <Provider store={store}> <App /> </Provider> )
|
4. React组件使用store中的数据
在React组件中使用store中的数据,需要用到一个钩子函数 - useSelector,它的作用是把store中的数据映射到组件中,使用样例如下:

5. React组件修改store中的数据
React组件中修改store中的数据需要借助另外一个hook函数 - useDispatch,它的作用是生成提交action对象的dispatch函数,使用样例如下:

Redux与React - 提交action传参
需求:组件中有俩个按钮 add to 10
和 add to 20
可以直接把count值修改到对应的数字,目标count值是在组件中传递过去的,需要在提交action的时候传递参数

实现方式:在reducers的同步修改方法中添加action对象参数,在调用actionCreater的时候传递参数,参数会被传递到action对象payload属性上

Redux与React - 异步action处理
需求理解

实现步骤
- 创建store的写法保持不变,配置好同步修改状态的方法
- 单独封装一个函数,在函数内部return一个新函数,在新函数中
2.1 封装异步请求获取数据
2.2 调用同步actionCreater传入异步数据生成一个action对象,并使用dispatch提交
- 组件中dispatch的写法保持不变
代码实现
测试接口地址: http://geek.itheima.net/v1_0/channels
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32
| import { createSlice } from '@reduxjs/toolkit' import axios from 'axios'
const channelStore = createSlice({ name: 'channel', initialState: { channelList: [] }, reducers: { setChannelList (state, action) { state.channelList = action.payload } } })
const { setChannelList } = channelStore.actions const url = 'http://geek.itheima.net/v1_0/channels'
const fetchChannelList = () => { return async (dispatch) => { const res = await axios.get(url) dispatch(setChannelList(res.data.data.channels)) } }
export { fetchChannelList }
const channelReducer = channelStore.reducer export default channelReducer
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| import { useEffect } from 'react' import { useSelector, useDispatch } from 'react-redux' import { fetchChannelList } from './store/channelStore'
function App () { const { channelList } = useSelector(state => state.channel) useEffect(() => { dispatch(fetchChannelList()) }, [dispatch])
return ( <div className="App"> <ul> {channelList.map(task => <li key={task.id}>{task.name}</li>)} </ul> </div> ) }
export default App
|
Redux官方提供了针对于Redux的调试工具,支持实时state信息展示,action提交信息查看等

美团小案例
1. 案例演示

基本开发思路:使用 RTK(Redux Toolkit)来管理应用状态, 组件负责 数据渲染 和 dispatch action
2. 准备并熟悉环境
克隆项目到本地(内置了基础静态组件和模版)
1
| git clone http://git.itcast.cn/heimaqianduan/redux-meituan.git
|
安装所有依赖
启动mock服务(内置了json-server)
启动前端服务
3. 分类和商品列表渲染

1- 编写store逻辑
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34
| import { createSlice } from "@reduxjs/toolkit" import axios from "axios"
const foodsStore = createSlice({ name: 'foods', initialState: { foodsList: [] }, reducers: { setFoodsList (state, action) { state.foodsList = action.payload } } })
const { setFoodsList } = foodsStore.actions const fetchFoodsList = () => { return async (dispatch) => { const res = await axios.get('http://localhost:3004/takeaway') dispatch(setFoodsList(res.data)) } }
export { fetchFoodsList }
const reducer = foodsStore.reducer
export default reducer
|
2- 组件使用store数据
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47
| import { useDispatch, useSelector } from 'react-redux' import { fetchFoodsList } from './store/modules/takeaway' import { useEffect } from 'react'
const App = () => { const dispatch = useDispatch() useEffect(() => { dispatch(fetchFoodsList()) }, [dispatch])
return ( <div className="home"> {/* 导航 */} <NavBar />
{/* 内容 */} <div className="content-wrap"> <div className="content"> <Menu /> <div className="list-content"> <div className="goods-list"> {/* 外卖商品列表 */} {foodsList.map(item => { return ( <FoodsCategory key={item.tag} // 列表标题 name={item.name} // 列表商品 foods={item.foods} /> ) })} </div> </div> </div> </div> {/* 购物车 */} <Cart /> </div> ) }
export default App
|
4. 点击分类激活交互实现

1- 编写store逻辑
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
|
import { createSlice } from "@reduxjs/toolkit" import axios from "axios"
const foodsStore = createSlice({ name: 'foods', initialState: { activeIndex: 0 }, reducers: { changeActiveIndex (state, action) { state.activeIndex = action.payload } } })
const { changeActiveIndex } = foodsStore.actions
export { changeActiveIndex }
const reducer = foodsStore.reducer
export default reducer
|
2- 编写组件逻辑
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
| const Menu = () => { const { foodsList, activeIndex } = useSelector(state => state.foods) const dispatch = useDispatch() const menus = foodsList.map(item => ({ tag: item.tag, name: item.name })) return ( <nav className="list-menu"> {/* 添加active类名会变成激活状态 */} {menus.map((item, index) => { return ( <div // 提交action切换激活index onClick={() => dispatch(changeActiveIndex(index))} key={item.tag} // 动态控制active显示 className={classNames( 'list-menu-item', activeIndex === index && 'active' )} > {item.name} </div> ) })} </nav> ) }
|
5. 商品列表切换显示

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| <div className="list-content"> <div className="goods-list"> {/* 外卖商品列表 */} {foodsList.map((item, index) => { return ( activeIndex === index && <FoodsCategory key={item.tag} // 列表标题 name={item.name} // 列表商品 foods={item.foods} /> ) })} </div> </div>
|
6. 添加购物车实现

1- 编写store逻辑
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29
|
import { createSlice } from "@reduxjs/toolkit" import axios from "axios"
const foodsStore = createSlice({ name: 'foods', reducers: { addCart (state, action) { const item = state.cartList.find(item => item.id === action.payload.id) if (item) { item.count++ } else { state.cartList.push(action.payload) } } } })
const { addCart } = foodsStore.actions
export { addCart }
const reducer = foodsStore.reducer
export default reducer
|
2- 编写组件逻辑
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| <div className="goods-count"> {} <span className="plus" onClick={() => dispatch(addCart({ id, picture, name, unit, description, food_tag_list, month_saled, like_ratio_desc, price, tag, count }))}></span> </div>
|
7. 统计区域实现

实现思路
- 基于store中的cartList的length渲染数量
- 基于store中的cartList累加price * count
- 购物车cartList的length不为零则高亮
1 2 3 4 5 6 7 8
| const totalPrice = cartList.reduce((a, c) => a + c.price * c.count, 0)
{} {} <div onClick={onShow} className={classNames('icon', cartList.length > 0 && 'fill')}> {cartList.length > 0 && <div className="cartCornerMark">{cartList.length}</div>} </div>
|
8. 购物车列表功能实现

1-控制列表渲染
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36
| const Cart = () => { return ( <div className="cartContainer"> {/* 添加visible类名 div会显示出来 */} <div className={classNames('cartPanel', 'visible')}> {/* 购物车列表 */} <div className="scrollArea"> {cartList.map(item => { return ( <div className="cartItem" key={item.id}> <img className="shopPic" src={item.picture} alt="" /> <div className="main"> <div className="skuInfo"> <div className="name">{item.name}</div> </div> <div className="payableAmount"> <span className="yuan">¥</span> <span className="price">{item.price}</span> </div> </div> <div className="skuBtnWrapper btnGroup"> {/* 数量组件 */} <Count count={item.count} /> </div> </div> ) })} </div> </div> </div> ) }
export default Cart
|
2- 购物车增减逻辑实现
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| increCount (state, action) { const item = state.cartList.find(item => item.id === action.payload.id) item.count++ },
decreCount (state, action) { const item = state.cartList.find(item => item.id === action.payload.id) if (item.count === 0) { return } item.count-- }
|
1 2 3 4 5 6 7 8
| <div className="skuBtnWrapper btnGroup"> {} <Count count={item.count} onPlus={() => dispatch(increCount({ id: item.id }))} onMinus={() => dispatch(decreCount({ id: item.id }))} /> </div>
|
3-清空购物车实现
1 2 3 4
| clearCart (state) { state.cartList = [] }
|
1 2 3 4 5 6 7 8
| <div className="header"> <span className="text">购物车</span> <span className="clearCart" onClick={() => dispatch(clearCart())}> 清空购物车 </span> </div>
|
9. 控制购物车显示和隐藏

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| const [visible, setVisible] = useState(false)
const onShow = () => { if (cartList.length > 0) { setVisible(true) } }
{} <div className={ classNames('cartOverlay', visible && 'visible') } onClick={() => setVisible(false)} />
|