引言
📒📒📒欢迎来到小冷的代码学习世界
博主的微信公众号 : 想全栈的小冷,分享一些技术上的文章,以及解决问题的经验
⏩当前专栏:react系列
Redux
完整代码案例仓库 :https://gitee.com/cold-abyss_admin/react-redux-meituan
Redux是 React 最常用的集中状态管理工具,类似与VUE的pinia(vuex) 可以独立于框架运行
使用思路:
- 定义一个
reducer
函数 根据当前想要做的修改返回一个新的状态 - 使用createStore方法传入reducer函数 生成一个store实例对象
- subscribe方法 订阅数据的变化(数据一旦变化,可以得到通知)
- dispatch方法提交action对象 告诉reducer你想怎么改数据
- getstate方法 获取最新的状态数据更新到视图中
配置Redux
在React中使用redux,官方要求安装俩个其他插件-和react-redux
官方推荐我们使用 RTK(ReduxToolkit) 这是一套工具集合 可以简化书写方式
- 简化store配置
- 内置immer可变式状态修改
- 内置thunk更好的异步创建
调试工具安装
谷歌浏览器搜索 redux-devtool安装 工具
依赖安装
#redux工具包
npm i @reduxjs/toolkit react-redux
#调试工具包
npm install --save-dev redux-devtools-extension
store目录机构设计
- 通常集中状态管理的部分都会单独创建一个
store
目录 - 应用通常会有多个子store模块,所以创建一个
modules
进行内部业务的区分 - store中的入口文件index.js 的作用是组合所有
modules
的子模块 并且导出store
快速上手
使用react+redux 开发一个计数器 熟悉一下技术
-
使用
Reacttoolkit
创建 counterStoreimport {createSlice} from "@reduxjs/toolkit"; const counterStore= createSlice({ name: "counter", // 初始化 state initialState: { count: 0 }, // 修改状态的方法 reducers:{ increment(state){ state.count++ }, decrement(state){ state.count-- } } }) // 解构函数 const {increment,decrement}= counterStore.actions // 获取reducer const reducer = counterStore.reducer; export {increment,decrement} export default reducer
-
在
index.js
集合counterimport {configureStore} from "@reduxjs/toolkit"; import counterStore from "./modules/counterStore"; const store = configureStore({ reducer:{ couner: counterStore, } }) export default store
-
为React 注入
store
,react-redux
负责把Redux和React链接 起来,内置Provider
组件 通过store
参数把创建好的store实例注入到应用中 找到项目中的index.js
const root = ReactDOM.createRoot(document.getElementById('root')); root.render( <React.StrictMode> <Provider store={store}> <App /> </Provider> </React.StrictMode> );
-
使用useSelector 获取到数据
import {useSelector} from "react-redux"; function App() { const {count} = useSelector(state => state.counter); return ( <div className="App"> {count} </div> ); }
-
使用 钩子函数
useDispatch
import {useDispatch, useSelector} from "react-redux"; import {inscrement,descrement} from "./store/modules/counterStore" function App() { const {count} = useSelector(state => state.counter); const dispatch = useDispatch() return ( <div className="App"> <button onClick={()=>dispatch(inscrement())}>+</button> {count} <button onClick={()=>dispatch(descrement())}>-</button> </div> ); } export default App;
-
查看效果
提交acntion传参
在reducers
的同步修改方法中添加action对象参数,在调用actionCreater
参数的时候传递参数,参数会被传递到action对象的payload
属性上
我们继续的改造一下counterStore
action
这个对象参数有个固定的属性叫payload用来接收传参
然后 app.js
添加两个按钮 用来传递参数
效果
Reudx action异步操作
区分同步和异步action
如果action的内容是 object对象那就是同步action,如果是函数 那就是异步action
为什么我们需要异步action操作来使用请求 ?
例子:
我们有两种方式可以实现 隔五分钟 上蛋炒饭
一种是客人自己思考五分钟
一种是客人点好 叫服务员五分钟之后上
这个服务员就是 redux 我们刚希望相关aciton的操作都在redux里完成这个时候同步action就不能满足我们的需求了 所以需要使用异步action
异步操作的代码变化不大,我们创建store的写法保持不变 ,但是在函数中用异步操作的时候需要一个能异步执行函数return出一个新的函数而我们的异步操作卸载新的函数中.
异步action中一般都会调用一个同步action
案例: 从后端获取到列表展示到页面
新建一个文件叫做 ChannelStore.js
然后编写对应的创建代码
import {createSlice} from "@reduxjs/toolkit";
import axios from "axios";
const channelStore = createSlice({
name: "channel",
initialState: {
channelList:[]
},
reducers:{
setChannel(state, action){
state.channelList=action.payload
}
}
})
const {setChannel}= channelStore.actions
// 异步请求
const fetchChannelList = ()=>{
return async (dispatch)=>{
const res = await axios.get('http://geek.itheima.net/v1_0/channels')
dispatch(setChannel(res.data.data.channels))
}
}
const reducer = channelStore.reducer;
export {fetchChannelList}
export default reducer
然后去store
入口加入channelStore
import {configureStore} from "@reduxjs/toolkit";
import counterStore from "./modules/counterStore";
import channelStore from "./modules/channelStore";
const store = configureStore({
reducer:{
counter: counterStore,
channel: channelStore,
}
})
export default store
之后就可以在app.js
加入代码
import {useDispatch, useSelector} from "react-redux";
import {useEffect} from "react";
import {fetchChannelList} from "./store/modules/channelStore";
function App() {
const {channelList} = useSelector(state => state.channel);
const dispatch = useDispatch()
useEffect(() => {
dispatch(fetchChannelList())
}, [dispatch]);
return (
<div className="App">
<ul>
{channelList.map(item =><li key={item.id}>{item.name}</li>)}
</ul>
</div>
);
}
export default App;
代码效果
redux hooks
useSelector
它的作用是吧store中的数据映射到组件中
const {count} = useSelector(state => state.counter);
这里的count其实对应的就是
useDispatch
它的作用是生成提交 action对象的dispatch函数
import {useDispatch, useSelector} from "react-redux";
import {inscrement,descrement} from "./store/modules/counterStore"
function App() {
const {count} = useSelector(state => state.counter);
const dispatch = useDispatch()
return (
<div className="App">
<button onClick={()=>dispatch(inscrement())}>+</button>
{count}
<button onClick={()=>dispatch(descrement())}>-</button>
</div>
);
}
export default App;
美团点餐界面小案例
下载模板地址:
git clone http://git.itcast.cn/heimaqianduan/redux-meituan.git
效果与功能列表展示
基本的思路就是使用 RTK 来做状态管理,组件负责数据渲染和操作action
我们在store文件夹下开始配置和编写store的使用逻辑
分类渲染
先编写对应的reducer 和异步请求逻辑
takeaway.js
用于异步请求列表数据
import {createStore} from './store';
import axios from "axios";
const foodsState = createStore({
name:'foods',
initialState: {
foodsList:[]
},
reducers:{
setFoodsList(state, action){
state.foodsList=action.payload
}
}
});
const {setFoodsList} = foodsState.actions;
//异步获取部分
const fetchFoodsList = () => {
return async dispatch => {
// 异步逻辑
const res = await axios.get(' http://localhost:3004/takeaway\n')
// 调用dispatch
dispatch(setFoodsList(res.data))
}
}
const reducer = foodsState.reducer
export {fetchFoodsList}
export default reducer
将子store管理起来 在store文件夹下编写一个index.js作为访问store的入口
import {configureStore} from "@reduxjs/toolkit";
import foodsReducer from './modules/takeaway'
const store= configureStore({
reducer:{
foods:foodsReducer
}
})
export default store
然后将redux和react连接起来 将store 注入进去 选择根目录的index.js
import React from 'react'
import { createRoot } from 'react-dom/client'
import { Provider } from 'react-redux'
import App from './App'
import store from "./store";
const root = createRoot(document.getElementById('root'))
root.render(
<Provider store={store}>
<App />
</Provider>
)
编写渲染页面
在app.js里 遵循步骤开始操作store
- 使用
useDispatch
函数取得对象 - 使用
useEffect
调用异步函数获取服务器数据 - 使用
useSelector
拿到数据并且循环展示
import NavBar from './components/NavBar'
import Menu from './components/Menu'
import Cart from './components/Cart'
import FoodsCategory from './components/FoodsCategory'
import './App.scss'
import {useSelector} from "react-redux";
const App = () => {
// 访问store拿到数据
const {foodsList} = useSelector(state => state.foods)
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
效果
侧边栏渲染.交互
我们需要在获取列表解构的时候 拿到属于左侧列表的数据
然后循环的展示在menu组件中 只需要把异步请求的数据放到menu组件中就可以展示侧边栏了
import classNames from 'classnames'
import './index.scss'
import {useDispatch, useSelector} from "react-redux";
const Menu = () => {
// 获取dispatch
const dispatch = useDispatch()
// 访问store拿到数据
const {foodsList} = useSelector(state => state.foods)
const menus = foodsList.map(item => ({ tag: item.tag, name: item.name }))
return (
<nav className="list-menu">
{/* 添加active类名会变成激活状态 */}
{menus.map((item, index) => {
return (
<div
key={item.tag}
className={classNames(
'list-menu-item',
'active'
)}
>
{item.name}
</div>
)
})}
</nav>
)
}
export default Menu
效果
接下来编写交互操作 使用RTK来管理activeindex
- 新增
activeIndex
并且设置好对应的同步操作action方法以及导出
import {createSlice} from '@reduxjs/toolkit';
import axios from "axios";
const foodsState = createSlice({
name:'foods',
initialState: {
// 商品列表
foodsList:[],
// 菜单激活值
activeIndex:0,
},
reducers:{
setFoodsList(state, action){
state.foodsList=action.payload
},
changeActiveIndex(state, action){
state.activeIndex=action.payload
}
}
});
const {setFoodsList,changeActiveIndex} = foodsState.actions;
//异步获取部分
const fetchFoodsList = () => {
return async dispatch => {
// 异步逻辑
const res = await axios.get(' http://localhost:3004/takeaway\n')
// 调用dispatch
dispatch(setFoodsList(res.data))
console.log(res.data)
}
}
const reducer = foodsState.reducer
export {fetchFoodsList,changeActiveIndex}
export default reducer
然后开始编写menu组件的点击效果
代码修改
menu/index.js
import classNames from 'classnames'
import './index.scss'
import {useDispatch, useSelector} from "react-redux";
import {changeActiveIndex} from "../../store/modules/takeaway";
const Menu = () => {
// 获取dispatch
const dispatch = useDispatch()
// 访问store拿到数据
const {foodsList,activeIndex} = useSelector(state => state.foods)
const menus = foodsList.map(item => ({ tag: item.tag, name: item.name }))
return (
<nav className="list-menu">
{/* 添加active类名会变成激活状态 */}
{menus.map((item, index) => {
return (
<div
onClick={()=>dispatch(changeActiveIndex(index))}
key={item.tag}
className={classNames(
'list-menu-item',
activeIndex===index&& 'active'
)}
>
{item.name}
</div>
)
})}
</nav>
)
}
export default Menu
效果
当点击的时候index就会切换到对应的index上 并且在点击当前index的时候选项高亮
商品列表的切换显示
点击侧边栏的时候 菜单栏需要显示对应侧边栏index的菜单
修改 app.js
菜单栏标签的显示规则就行
const App = () => {
// 获取dispatch
const dispatch = useDispatch()
// 异步请求数据
useEffect(() => {
dispatch(fetchFoodsList())
}, [dispatch]);
// 访问store拿到数据
const {foodsList,activeIndex} = useSelector(state => state.foods)
return (
<div className="home">
{/* 导航 */}
<NavBar />
{/* 内容 */}
<div className="content-wrap">
<div className="content">
<Menu />
<div className="list-content">
<div className="goods-list">
{/* 外卖商品列表 */}
{foodsList.map((item,index) => {
return (
index===activeIndex&& <FoodsCategory
key={item.tag}
// 列表标题
name={item.name}
// 列表商品
foods={item.foods}
/>
)
})}
</div>
</div>
</div>
</div>
{/* 购物车 */}
<Cart />
</div>
)
}
添加购物车
首先找到fooditem中的food对象 一会我们使用cartlist的时候要用到 id 和count
使用 RTK管理 状态cartlist
import {createSlice} from '@reduxjs/toolkit';
import axios from "axios";
const foodsState = createSlice({
name:'foods',
initialState: {
// 商品列表
foodsList:[],
// 菜单激活值
activeIndex:0,
// 购物车列表
cartList:[]
},
reducers:{
// 修改商品列表
setFoodsList(state, action){
state.foodsList=action.payload
},
// 更改activeIndex
changeActiveIndex(state, action){
state.activeIndex=action.payload
},
// 添加购物车
addCart(state, action){
// 通过payload.id去匹配cartList匹配,匹配到代表添加过
const item = state.cartList.find(item=>item.id ===action.payload.id)
if (item){
item.count++
}else{
state.cartList.push(action.payload)
}
}
}
});
const {setFoodsList,changeActiveIndex,addCart} = foodsState.actions;
//异步获取部分
const fetchFoodsList = () => {
return async dispatch => {
// 异步逻辑
const res = await axios.get(' http://localhost:3004/takeaway\n')
// 调用dispatch
dispatch(setFoodsList(res.data))
console.log(res.data)
}
}
const reducer = foodsState.reducer
export {fetchFoodsList,changeActiveIndex,addCart}
export default reducer
在fooditem.jsx
编写cartList触发操作
- 要记得给 count一个默认值 不然会是 null
- 修改 classname为plus的span标签新增点击事件
import './index.scss'
import {useDispatch} from "react-redux";
import {addCart} from "../../../store/modules/takeaway";
const Foods = ({
id,
picture,
name,
unit,
description,
food_tag_list,
month_saled,
like_ratio_desc,
price,
tag,
count =1
}) => {
const dispatch = useDispatch()
return (
<dd className="cate-goods">
<div className="goods-img-wrap">
<img src={picture} alt="" className="goods-img" />
</div>
<div className="goods-info">
<div className="goods-desc">
<div className="goods-title">{name}</div>
<div className="goods-detail">
<div className="goods-unit">{unit}</div>
<div className="goods-detail-text">{description}</div>
</div>
<div className="goods-tag">{food_tag_list.join(' ')}</div>
<div className="goods-sales-volume">
<span className="goods-num">月售{month_saled}</span>
<span className="goods-num">{like_ratio_desc}</span>
</div>
</div>
<div className="goods-price-count">
<div className="goods-price">
<span className="goods-price-unit">¥</span>
{price}
</div>
<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>
</div>
</div>
</dd>
)
}
export default Foods
效果
统计订单区域
实现思路
- 基于store中的cartList的length渲染数量
- 基于store中的cartList累加price * count
- 购物车cartList的length不为零则高亮
- 设置总价
// 计算总价
const totalPrice = cartList.reduce((a, c) => a + c.price * c.count, 0)
{/* fill 添加fill类名购物车高亮*/}
{/* 购物车数量 */}
<div onClick={onShow} className={classNames('icon', cartList.length > 0 && 'fill')}>
{cartList.length > 0 && <div className="cartCornerMark">{cartList.length}</div>}
</div>
效果
cart.jsx
全部代码
import classNames from 'classnames'
import Count from '../Count'
import './index.scss'
import {useSelector} from "react-redux";
import {fill} from "lodash/array";
const Cart = () => {
const{cartList} = useSelector(state => state.foods)
// 计算总价
const totalPrice = cartList.reduce((a, c) => a+c.price*c.count,0)
const cart = []
return (
<div className="cartContainer">
{/* 遮罩层 添加visible类名可以显示出来 */}
<div
className={classNames('cartOverlay')}
/>
<div className="cart">
{/* fill 添加fill类名可以切换购物车状态*/}
{/* 购物车数量 */}
<div className={classNames('icon')}>
{cartList.length>0 && <div className="cartCornerMark">{cartList.length}</div>}
</div>
{/* 购物车价格 */}
<div className="main">
<div className="price">
<span className="payableAmount">
<span className="payableAmountUnit">¥</span>
{totalPrice.toFixed(2)}
</span>
</div>
<span className="text">预估另需配送费 ¥5</span>
</div>
{/* 结算 or 起送 */}
{cartList.length > 0 ? (
<div className="goToPreview">去结算</div>
) : (
<div className="minFee">¥20起送</div>
)}
</div>
{/* 添加visible类名 div会显示出来 */}
<div className={classNames('cartPanel')}>
<div className="header">
<span className="text">购物车</span>
<span className="clearCart">
清空购物车
</span>
</div>
{/* 购物车列表 */}
<div className="scrollArea">
{cart.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
购物车列表功能
修改
takeaway.js
内容如下 :
- 新增加减购物车内的视频数量
- 清楚购物车
- 只有一项时删除商品选择
import {createSlice} from '@reduxjs/toolkit';
import axios from "axios";
const foodsState = createSlice({
name:'foods',
initialState: {
// 商品列表
foodsList:[],
// 菜单激活值
activeIndex:0,
// 购物车列表
cartList:[]
},
reducers:{
// 修改商品列表
setFoodsList(state, action){
state.foodsList=action.payload
},
// 更改activeIndex
changeActiveIndex(state, action){
state.activeIndex=action.payload
},
// 添加购物车
addCart(state, action){
// 通过payload.id去匹配cartList匹配,匹配到代表添加过
const item = state.cartList.find(item=>item.id ===action.payload.id)
if (item){
item.count++
}else{
state.cartList.push(action.payload)
}
},
// count增
increCount(state, action){
const item = state.cartList.find(item=>item.id ===action.payload.id)
item.count++
},
// count减
decreCount(state, action){
const item = state.cartList.find(item=>item.id ===action.payload.id)
// 只有一项的时候将商品移除购物车
if (item.count <=1){
state.cartList= state.cartList.filter(item=>item.id !=action.payload.id)
return
}
item.count--
},
// 清除购物车
clearCart(state){
state.cartList=[]
}
}
});
const {clearCart,decreCount,increCount,setFoodsList,changeActiveIndex,addCart} = foodsState.actions;
//异步获取部分
const fetchFoodsList = () => {
return async dispatch => {
// 异步逻辑
const res = await axios.get(' http://localhost:3004/takeaway\n')
// 调用dispatch
dispatch(setFoodsList(res.data))
console.log(res.data)
}
}
const reducer = foodsState.reducer
export {fetchFoodsList,changeActiveIndex,addCart,clearCart,decreCount,increCount}
export default reducer
购物车列表的显示和隐藏
- 使用usestate设置一个状态
- 点击统计的时候就展示
- 点击蒙层就不显示
import classNames from 'classnames'
import Count from '../Count'
import './index.scss'
import {useDispatch, useSelector} from "react-redux";
import {clearCart, decreCount, increCount} from "../../store/modules/takeaway";
import {useState} from "react";
const Cart = () => {
const dispatch =useDispatch()
const{cartList} = useSelector(state => state.foods)
// 计算总价
const totalPrice = cartList.reduce((a, c) => a+c.price*c.count,0)
const[visible,setVisible]=useState(false)
return (
<div className="cartContainer">
{/* 遮罩层 添加visible类名可以显示出来 */}
<div
onClick={()=>setVisible(false)}
className={classNames('cartOverlay',visible&&'visible')}
/>
<div className="cart">
{/* fill 添加fill类名可以切换购物车状态*/}
{/* 购物车数量 */}
<div onClick={()=>setVisible(cartList.length!=0)} className={classNames('icon')}>
{cartList.length>0 && <div className="cartCornerMark">{cartList.length}</div>}
</div>
{/* 购物车价格 */}
<div className="main">
<div className="price">
<span className="payableAmount">
<span className="payableAmountUnit">¥</span>
{totalPrice.toFixed(2)}
</span>
</div>
<span className="text">预估另需配送费 ¥5</span>
</div>
{/* 结算 or 起送 */}
{cartList.length > 0 ? (
<div className="goToPreview">去结算</div>
) : (
<div className="minFee">¥20起送</div>
)}
</div>
{/* 添加visible类名 div会显示出来 */}
<div className={classNames('cartPanel',visible&&'visible')}>
<div className="header">
<span className="text">购物车</span>
<span onClick={()=>dispatch(clearCart())} className="clearCart">
清空购物车
</span>
</div>
{/* 购物车列表 */}
<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
onPlus={()=>dispatch(increCount({id:item.id}))}
count={item.count}
onMinus={()=>dispatch(decreCount({id:item.id}))}
/>
</div>
</div>
)
})}
</div>
</div>
</div>
)
}
export default Cart
到这里redux的入门, 实践, 小案例就完成了 之后可能会更新一些关于redux底层原理的文章 会加入到其中