0. 概述
基础特点
-
单向数据流,view 发出 action,store 调用 reducer 计算出新的 state,若 state 产生变化,则调用监听函数重新渲染 view
-
单一数据源,只有一个 Store
-
state 是只读的,每次状态更新之后只能返回一个新的 state
-
没有 dispatcher,而是在 store 中集成了 dispatch 方法,store.dispatch() 是view 发出 action 的唯一途径
-
支持使用中间件(Middleware)管理异步数据流
redux 的发展
redux 是一个独立的第三方库,之后 react 官方在 redux 的基础上推出了 react-redux,全面拥抱 hooks。
此外,redux 官方也推出了 redux toolkit,简化使用 redux 的过程,因为一般在 react 应用中,我们使用 react-redux + redux toolkit 。
1. Redux 快速上手
Redux 是 React 最常用的集中状态管理工具,类似于Vue中的Pinia(Vuex),可以独立于框架运行。
npm i redux
<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
// state 管理数据的初始状态
// action 对象的 type 属性标记需要做的修改操作
function reducer (state = { count: 0 }, action) {
switch (action.type) {
case 'INCREMENT':
// state 是对象,所以返回的数据也是对象
return { count: state.count + 1 }
case 'DECREMENT':
return { count: state.count - 1 }
default:
return state
}
}
// 2. 使用reducer函数生成store实例
const store = Redux.createStore(reducer)
// 3. 通过 store 实例的 subscribe 订阅数据变化
// 回调函数在每一次 state 发生变化时自动执行
store.subscribe(() => {
console.log(store.getState())
document.getElementById('count').innerText = store.getState().count
})
// 4. 通过 store 的 dispatch 函数提交 action 的更改状态
const inBtn = document.getElementById('increment')
inBtn.addEventListener('click', () => {
// 匹配的是 action 对象,所以传入 action 对象
store.dispatch({
type: 'INCREMENT'
})
})
// 减
const dBtn = document.getElementById('decrement')
dBtn.addEventListener('click', () => {
store.dispatch({
type: 'DECREMENT'
})
})
</script>
2. React 中使用 Redux
React Redux 官方入门文档:https://react-redux.js.org/tutorials/quick-start
由于我们使用的是 TypeScript,还要参考 TypeScript 的快速启动文档:https://react-redux.js.org/tutorials/typescript-quick-start
。
2.1 配制环境
- Redux Toolkit(RTK)- 官方推荐编写Redux逻辑的方式,简化书写方式
- react-redux - 用来链接 Redux 和 React组件的中间件
npx create-react-app react-redux-demo
npm i @reduxjs/toolkit react-redux
2.2 使用 RTK + react-redux
Slice 是 Redux Toolkit 中的概念,它将状态和相关的 reducer 逻辑组织在一起,便于模块化管理。每个 slice 通常代表应用中的一部分状态(如用户、产品、购物车等)。
在没有 Redux Toolkit 和 Slice 之前,传统的 Redux 开发需要定义 action types、action creators 和 reducer 函数,所有这些通常需要在不同的文件中编写,增加了代码的复杂性和维护成本。
创建 counterStore
// @/store/modules/counterStore.js
import { createSlice } from '@reduxjs/toolkit'
const counterStore = createSlice({
// 模块名称唯一
name: 'counter',
// 初始 state
initialState: {
count: 1
},
// 修改数据的同步方法 支持直接修改
reducers: {
increment (state) {
state.count++
},
decrement(state){
state.count--
},
addToNum(state, action) {
state.count = action.payload
}
}
})
// 解构出 actionCreater 函数
// action 具有两个值 一个是 payload 另一个是 type
// 也很好理解:一个行为具有行为的类型(type)和具体的实施(payload 传递值)
const { increment,decrement, addToNum } = counterStore.actions
// 获取 reducer 函数
const counterReducer = counterStore.reducer
// 导出
export { increment, decrement, addToNum }
export default counterReducer
疑问一:reducers 是什么东西?state(状态数据) 和 action(操作状态数据的行为) 和它又有什么关系?
疑问二:为什么只导出 reducer ,createSlice 和 configureStore 的关系是什么?
先说说英文里面他们仨都是什么意思:
-
reducer:归纳(这里是指归纳了 state 和 action 的一个东西)
-
state:状态
-
action:行为
/** * 详细讲讲这个抽象的东西 * reducers 对象中的函数 相当于一个函数中的一个个根据不同 action 执行的 case 语句 * reducer(state = initialState, action) { * case "ADD": * // 获取 action 根据 action 修改 state * return state + action.payload; * case "MINUS": * return state - action.payload; * } */
所以在 slice 分片的数据中,我们需要给 store 只是一个 reducer ,一个归纳好的东西,就不要导出 state 和 action了。
createSlice 返回值是一个对象,里面包含所有的 actions。
而 state 又是 和 action 紧密关联的。
所以这时我们只需要导出给 store 这个大仓库中,即可获取和操作所有的状态数据。
疑问三:导出的 actions 为什么不能直接函数调用呢?
每一个函数都有自己的作用域,同时具有自己的参数和返回值。
那么现在有一个问题:参数和返回值谁来保证?
答案是 patch ,所以就有了一句话:派发行为(patch ==> action)
疑问四:异步请求的处理
- 这里确实是 redux 比较麻烦的一个地方,默认 redux 不能在 reducers 中处理异步,而在外部处理 或者使用自带的一个方法 createAsyncThunk 。
- createAsyncThunk 可以被认为是一个 action,只不过它是异步的。进而正常通过 dispatch 派发即可。
- 但是,提到异步,就免不了有状态产生(pending/fulfilled/rejected),所以结果并不能被 reducers 正常归纳处理。
- 这时就需要引入 extraReducers 专门处理异步 action。它通过 builder 来添加 case,可以在每一个状态执行该状态下的 action。(两种写法参考官方文档:https://toolkit.redux.js.cn/api/createSlice/)
// @/store/modules/channelStore.js
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
// 相当于在 redux 外部处理异步(执行异步请求)
const fetchChannlList = () => {
return async (dispatch) => {
const res = await axios.get('http://geek.itheima.net/v1_0/channels')
dispatch(setChannels(res.data.data.channels))
}
}
export { fetchChannlList }
const reducer = channelStore.reducer
export default reducer
另一种处理异步请求的方式(redux 内部处理请求):
import {createAsyncThunk, createSlice} from "@reduxjs/toolkit";
import {getType} from "../../api/type";
export const getTypeList = createAsyncThunk(
'type/getTypeList',
async () => {
const response = await getType()
return response.data.data
}
)
const typeStore = createSlice({
name: 'type',
initialState: {
// 存储所有的类型
typeList: []
},
reducers: {},
// 处理异步的 reducer
extraReducers: (builder) => {
builder.addCase(getTypeList.fulfilled, (state, action) => {
state.typeList = action.payload
})
}
})
export default typeStore.reducer
在项目的 src 目录下新建 stores 目录,用于存放所有的状态。然后在 stores 目录下新建 index.ts 文件,创建一个 Redux Store:
// @/store/index.js
import { configureStore } from '@reduxjs/toolkit'
import counterReducer from './modules/counterStore'
export default configureStore({
reducer: {
// 注册子模块
counter: counterReducer
}
})
// store.getStore 是一个返回 reducer 类型的函数 返回值是 reducer 类型
export type IRootState = ReturnType<typeof store.getState>;
export type IRootDispatch = typeof store.dispatch;
// 自定义绑定类型的 useSelector 和 useDispatch
export const useAppSelector: TypedUseSelectorHook<IRootState> = useSelector;
export const useAppDispatch: () => IRootDispatch = useDispatch;
为 React 注入 store
// @/index.js
import React from 'react'
import ReactDOM from 'react-dom/client'
import App from './App'
// 导入store
import store from './store'
// 导入store提供组件Provider
import { Provider } from 'react-redux'
ReactDOM.createRoot(document.getElementById('root')).render(
// 提供store数据
<Provider store={store}>
<App />
</Provider>
)
如果是 Next 项目:
import store from '@/stores'
import { Provider } from 'react-redux'
export default function RootLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
return (
<html lang="en">
<body>
<AntdRegistry>
<Provider store={store}>
<BasicLayout>{children}</BasicLayout>
</Provider>
</AntdRegistry>
</body>
</html>
);
}
在 React 组件中使用修改 store 中的数据
// App.js
import { useEffect } from 'react'
import { useDispatch, useSelector } from 'react-redux'
// 导入actionCreater
import { inscrement, decrement, addToNum } from './store/modules/counterStore'
import { fetchChannlList } from './store/modules/channelStore'
function App () {
// useSelector 函数将 store 中的数据映射到组件中 counter 是 store 名字
const { count } = useSelector(state => state.counter)
const { channelList } = useSelector(state => state.channel)
const dispatch = useDispatch()
// 使用useEffect触发异步请求执行
useEffect(() => {
dispatch(fetchChannlList())
}, [dispatch])
return (
<div className="App">
<button onClick={() => dispatch(decrement())}>-</button>
{count}
<button onClick={() => dispatch(inscrement())}>+</button>
{/* 变为10 和 变为20 */}
<button onClick={() => dispatch(addToNum(10))}>add To 10</button>
<button onClick={() => dispatch(addToNum(20))}>add To 20</button>
<ul>
{channelList.map(item => <li key={item.id}>{item.name}</li>)}
</ul>
</div>
)
}
export default App
结合 ts 使用:
import { configureStore } from "@reduxjs/toolkit";
import counter from "@/store/modules/counter";
import { TypedUseSelectorHook, useSelector } from "react-redux";
const store = configureStore({
reducer: {
counter
}
});
// store.getStore 为一个返回 reducer 类型的函数 返回值是 reducer 类型
export type IRootState = ReturnType<typeof store.getState>;
// 自定义绑定类型的 useSelector
export const useAppSelector: TypedUseSelectorHook<IRootState> = useSelector;
export default store;
2.3 总结
-
createSlice:用来接收 reducer 函数的对象、切片名称和初始状态值,并自动生成切片 reducer, 并带有相应的actions。
-
configureStore: 封装 createStore 以提供简化的配置选项和良好的默认值。
-
createAsyncThunk: 接收一个动作类型字符串和一个返回 Promise 的函数,并生成一个 pending/fulfilled/rejected 基于该 Promise 分派动作类型的 thunk。
// 以下三个参数必须传递
const channelStore = createSlice({
name: 'channel',
initialState: {
channelList: []
},
reducers: {
setChannels(state, action) {
state.channelList = action.payload
},
}
})
// 解构出来 actionCreator 函数(修改状态数据的函数)
const {setChannels} = channelStore.actions
// 获取 reducer
const reducer = channelStore.reducer
// 以按需导出的方式导出 actionCreator 生成 action 对象 的函数
export {setChannels}
// 导出 reducer 函数
export default reducer
const store = configureStore({
// 接收 子 reducer 并导入合并
reducer: {
counter: counterReducer,
channel: channelReducer
}
})
export default store
function App() {
// 获取 reducer 并解构出 state
const {count} = useSelector(state => state.counter)
// 生成提交 action 对象的 dispatch 函数,用于辅助修改 state
const dispatch = useDispatch()
const {channelList} = useSelector(state => state.channel)
// useEffect 触发异步请求执行
useEffect(() => {
dispatch(fetchChannelList())
}, [dispatch]);
return (
<div>
{/*dispatch 触发 actionCreater 修改数据*/}
<button onClick={() => dispatch(decrement())}> -</button>
{count}
<button onClick={() => dispatch(increment())}> +</button>
<button onClick={() => dispatch(addToNum(10))}> + 10</button>
<ul>
{channelList.map(item => (
<li key={item.id}>{item.name}</li>
))}
</ul>
</div>
);
}
- initialState 初始化 state(数据状态)
- reducers 修改状态数据的函数
接收两个参数 state 和 action。而 action.payload 是传入的参数。
组合 redux 和 react:
<Provider store={store}>
<App />
</Provider>
异步操作
- 配置同步修改状态的方法
- 单独封装一个函数,在函数内部return一个新函数,在新函数中
2.1 封装异步请求获取数据
2.2调用同步actionCreator传入异步数据生成一个action对象,并使用dispatch提交
异步获取数据,同步修改数据
const fetchChannelList = () => {
// 这里可以直接使用 dispatch
return async (dispatch)=> {
const res = await axios.get('http://geek.itheima.net/v1_0/channels')
dispatch(setChannels(res.data.data.channels))
}
}
export {fetchChannelList}