06-Redux
Redux 是 React 最常用的集中状态管理工具,类似于 Vue 中的 Pinia(Vuex),可以独立于框架运行
作用:通过集中管理的方式管理应用的状态
为什么要使用 Redux?
- 独立于组件,无视组件之间的层级关系,简化通信问题
- 单项数据流清晰,易于定位 bug
- 调试工具配套良好,方便调试
1. 快速体验
1. 实现计数器
需求:不和任何框架绑定,不使用任何构建工具,使用纯 Redux 实现计数器
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<button id="decrement">-</button>
<span id="count">0</span>
<button id="increment">+</button>
</body>
<script src="https://cdn.jsdelivr.net/npm/redux@4.2.0/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. 使用 createStore 函数生成 store 实例
const store = Redux.createStore(counterReducer)
// 3. 订阅数据变化(一旦变化,可以得到通知)
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>
</html>
2. Redux数据流架构
Redux 难点:理解它对于数据修改的规则,下图动态展示了在整个数据的修改中,数据的流向
为了职责清晰,Redux 代码被分为三个核心的概念:
state
:一个对象,存放着需要管理的数据action
:一个对象,用来描述怎样改数据reducer
:一个函数,根据 action 的描述更新 state
2. 环境准备
Redux 虽然是一个框架无关可以独立运行的插件,但是社区通常把它与 React 绑定在一起使用。计数器案例(Redux + React)
1. 配套工具
在 React 中使用 Redux,官方要求安装两个其他插件:
Redux Toolkit
、react-redux
Redux Toolkit(RTK)
:官方推荐编写 Redux 逻辑的方式,是一套工具的集合集,简化书写方式react-redux
:用来链接 Redux 和 React 组件的中间件
2. 基础环境
# 1. 使用 CRA 快速创建 React 项目
npx create-react-app react-redux
# 2. 安装配套工具
npm i @reduxjs/toolkit react-redux
# 3. 启动项目
npm run start
3. store目录结构设计
- 通常集中状态管理部分,都会单独创建一个单独的
store
目录 - 应用通常会有很多个子
store
模块,创建modules
目录,在内部编写业务分类的子store
store
中的入口文件index.js
- 作用:是组合
modules
中所有的子模块,并导出store
- 作用:是组合
3. 实现counter
1. 整体路径熟悉
2. counterStore.js
import {createSlice} from '@reduxjs/toolkit'
const counterStore = createSlice({
// 模块名称独一无二
name: 'counter',
// 初始数据
initialState: {
count: 1
},
// 修改数据的同步方法
reducers: {
increment(state) {
state.count++
},
decrement(state) {
state.count--
}
}
})
// 结构出 actionCreater
const {increment, decrement} = counterStore.actions
// 获取 reducer 函数
const counterReducer = counterStore.reducer
// 导出
export {increment, decrement}
export default counterReducer
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 实例注入到应用中,链接正式建立
import React from 'react';
import ReactDOM from 'react-dom/client';
import './index.css';
import App from './App';
import reportWebVitals from './reportWebVitals';
// 导入 store
import store from './store'
// 导入 store 提供组件 Provider
import {Provider} from 'react-redux'
ReactDOM.createRoot(document.getElementById('root')).render(
// 提供 store 数据
<Provider store={store}>
<App/>
</Provider>
)
// const root = ReactDOM.createRoot(document.getElementById('root'));
// root.render(
// <React.StrictMode>
// <App />
// </React.StrictMode>
// );
// If you want to start measuring performance in your app, pass a function
// to log results (for example: reportWebVitals(console.log))
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
reportWebVitals();
4. 组件中使用store中数据
在 React 组件中使用 store 中的数据,需要用到一个钩子函数
useSelector
- 作用:把 store 中的数据映射到组件中
5. 组件中修改store中数据
React 组件中修改 store 中的数据,需要借助另外一个 hook 函数
useDispatch()
- 作用:生成提交 action 对象的 dispatch 函数
import './App.scss'
import {useDispatch, useSelector} from 'react-redux';
// 导入创建 action 对象的方法
import {decrement, increment} from './store/modules/counterStore';
function App() {
const {count} = useSelector(state => state.counter);
// 得到 dispatch 函数
const dispatch = useDispatch();
return (
<div className="App">
{/* 调用 dispatch 提交 action 对象 */}
<button onClick={() => dispatch(decrement())}>-</button>
<span>{count}</span>
<button onClick={() => dispatch(increment())}>+</button>
</div>
);
}
export default App
4. 提交action传参
需求:组件中两个按钮
add to 10
、add to 20
直接把 count 值修改到对应的数字,count 值在组件中传递过去的,需要在提交 action 时传递参数
实现方式:在 reducers 的同步修改方法中添加 action 对象参数,在调用 actionCreater 时传递参数,参数会被传递到 action 对象 payload 属性上
import {createSlice} from "@reduxjs/toolkit"
const counterStore = createSlice({
name: 'counter',
// 初始化state
initialState: {
count: 0
},
// 修改状态的方法,同步方法,支持直接修改
reducers: {
inscrement(state) {
state.count++
},
decrement(state) {
state.count--
},
addToNum(state, action) {
state.count = action.payload
}
}
})
// 解构出来 actionCreater 函数
const {inscrement, decrement, addToNum} = counterStore.actions
// 获取 reducer
const reducer = counterStore.reducer
// 以按需导出的方式导出 actionCreater
export {inscrement, decrement, addToNum}
// 以默认导出的方式导出 reducer
export default reducer
import './App.scss'
import {useDispatch, useSelector} from 'react-redux';
// 导入创建 action 对象的方法
import {addToNum, decrement, inscrement} from './store/modules/counterStore';
function App() {
const {count} = useSelector(state => state.counter)
const dispatch = useDispatch()
// 使用useEffect触发异步请求执行
return (
<div className="App">
<button onClick={() => dispatch(decrement())}>-</button>
{count}
<button onClick={() => dispatch(inscrement())}>+</button>
<button onClick={() => dispatch(addToNum(10))}>add To 10</button>
<button onClick={() => dispatch(addToNum(20))}>add To 20</button>
</div>
)
}
export default App
5. 异步action处理
需求:
- 创建 store 的写法保持不变,配置好同步修改状态的方法
- 单独封装一个函数,在函数内部 return 一个新函数,在新函数中
- 封装异步请求获取数据
- 调用同步 actionCreater 传入异步数据生成一个 action 对象,并使用 dispatch 提交
- 组件中 dispatch 的写法保持不变
import {createSlice} from "@reduxjs/toolkit"
import axios from "axios"
const channelStore = createSlice({
name: 'channel',
initialState: {
channelList: []
},
reducers: {
setChannels(state, action) {
state.channelList = action.payload
}
}
})
// 异步请求部分
const {setChannels} = channelStore.actions
const fetchChannlList = () => {
return async (dispatch) => {
// const res = await axios.get('http://geek.itheima.net/v1_0/channels')
const res = {
"data": {
"channels": [
{"id": 0, "name": "推荐"}, {"id": 1, "name": "html"}, {"id": 2, "name": "开发者资讯"},
{"id": 4, "name": "c++"}, {"id": 6, "name": "css"}, {"id": 7, "name": "数据库"},
{"id": 8, "name": "区块链"}, {"id": 9, "name": "go"}, {"id": 10, "name": "产品"},
{"id": 11, "name": "后端"}, {"id": 12, "name": "linux"}, {"id": 13, "name": "人工智能"},
{"id": 14, "name": "php"}, {"id": 15, "name": "javascript"}, {"id": 16, "name": "架构"},
{"id": 17, "name": "前端"}, {"id": 18, "name": "python"}, {"id": 19, "name": "java"},
{"id": 20, "name": "算法"}, {"id": 21, "name": "面试"}, {"id": 22, "name": "科技动态"},
{"id": 23, "name": "js"}, {"id": 24, "name": "设计"}, {"id": 25, "name": "数码产品"},
{"id": 26, "name": "软件测试"}
]
}, "message": "OK"
}
dispatch(setChannels(res.data.channels))
}
}
export {fetchChannlList}
const reducer = channelStore.reducer
export default reducer
import {configureStore} from '@reduxjs/toolkit'
import channelReducer from './modules/channelStore'
export default configureStore({
reducer: {
// 注册子模块
channel: channelReducer
}
})
import {createContext, useContext, useEffect, useRef, useState} from 'react'
import './App.scss'
import {useDispatch, useSelector} from 'react-redux';
import {fetchChannlList} from './store/modules/channelStore'
function App() {
const {channelList} = useSelector(state => state.channel)
const dispatch = useDispatch()
// 使用 useEffect 触发异步请求执行
useEffect(() => {
dispatch(fetchChannlList())
}, [dispatch])
return (
<div className="App">
<ul>
{channelList.map(item => <li key={item.id}>{item.name}</li>)}
</ul>
</div>
)
}
export default App
6. Redux调试devtools
Redux 官方提供了针对于 Redux 的调试工具,支持实时 state 信息展示,action 提交信息查看等
7. 美团案例
1. 案例演示
基本开发思路:使用 RTK(Redux Toolkit)来管理应用状态,组件负责数据渲染和 dispatch action
2. 环境
# 1. 克隆项目到本地(内置了基础静态组件和模版)
git clone http://git.itcast.cn/heimaqianduan/redux-meituan.git
# 2. 安装所有依赖
npm i
# 3. 启动 mock 服务(内置了 json-server)
npm run serve
# 4. 启动前端服务
npm run start
3. 分类和商品列表渲染
- 编写 store 逻辑
// 编写 store
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 函数提交 action
dispatch(setFoodsList(res.data))
}
}
export { fetchFoodsList }
const reducer = foodsStore.reducer
export default reducer
- 组件使用 store 数据
// 省略部分代码
import { useDispatch, useSelector } from 'react-redux'
import { fetchFoodsList } from './store/modules/takeaway'
import { useEffect } from 'react'
const App = () => {
// 触发 action 执行
// 1. useDispatch -> dispatch 2. actionCreater导入进来 3.useEffect
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. 点击分类激活交互
- 编写 store 逻辑
// 编写 store
import { createSlice } from "@reduxjs/toolkit"
import axios from "axios"
const foodsStore = createSlice({
name: 'foods',
initialState: {
// 菜单激活下标值
activeIndex: 0
},
reducers: {
// 更改 activeIndex
changeActiveIndex (state, action) {
state.activeIndex = action.payload
}
}
})
// 导出
const { changeActiveIndex } = foodsStore.actions
export { changeActiveIndex }
const reducer = foodsStore.reducer
export default reducer
- 编写组件逻辑
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. 商品列表切换显示
<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. 添加购物车
- 编写 store 逻辑
import { createSlice } from "@reduxjs/toolkit"
import axios from "axios"
const foodsStore = createSlice({
name: 'foods',
reducers: {
// 添加购物车
addCart (state, action) {
// 是否添加过?以 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)
}
}
}
})
// 导出 actionCreater
const { addCart } = foodsStore.actions
export { addCart }
const reducer = foodsStore.reducer
export default reducer
- 编写组件逻辑
<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 不为零则高亮
// 计算总价
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>
8. 购物车列表功能
- 控制列表渲染
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
- 购物车增减逻辑实现
// count 增
increCount (state, action) {
// 关键点:找到当前要修改谁的 count id
const item = state.cartList.find(item => item.id === action.payload.id)
item.count++
},
// count 减
decreCount (state, action) {
// 关键点:找到当前要修改谁的 count id
const item = state.cartList.find(item => item.id === action.payload.id)
if (item.count === 0) {
return
}
item.count--
}
<div className="skuBtnWrapper btnGroup">
{/* 数量组件 */}
<Count
count={item.count}
onPlus={() => dispatch(increCount({ id: item.id }))}
onMinus={() => dispatch(decreCount({ id: item.id }))}
/>
</div>
- 清空购物车实现
// 清除购物车
clearCart (state) {
state.cartList = []
}
<div className="header">
<span className="text">购物车</span>
<span
className="clearCart"
onClick={() => dispatch(clearCart())}>
清空购物车
</span>
</div>
9. 购物车显示和隐藏
// 控制购物车打开关闭的状态
const [visible, setVisible] = useState(false)
const onShow = () => {
if (cartList.length > 0) {
setVisible(true)
}
}
{/* 遮罩层,添加 visible 类名可以显示出来 */}
<div
className={
classNames('cartOverlay', visible && 'visible')
}
onClick={() => setVisible(false)}
/>