本章记录一下
Redux
的学习过程,对于状态管理库,之前用的比较多的就是Vuex
和Pinia
,换到React
之后,随即而来的也是适合它的状态库。后续的话会接触到Recoil
。
简单的来说下为什么需要 redux
? JavaScript
的应用程序变得越来越复杂,状态也越来越多,包括了服务端返回的数据、缓存的数据等。管理这些状态因此也变得比较困难,并且状态之间也存在依赖关系,React
只是在视图层帮我们解决了 DOM
的渲染过程,但是 State
依然是留给自己来管理的,因此无论是组件自己定义的 state
还是组件之间通过 props
进行通信,都是我们自己来维护 state
。这个时候我们可以借助 Redux
、Recoil
这样的状态容器进行管理 state
。
Keywords
action
action
是一个具有 type
字段的普通 JavaScript
对象,用于描述应用程序中发生了什么的事件,所有需要改变 state
的内容,都需要用 action
,将 action
传入到 reducer
进行修改 state
,例如:
const addTodoAction = {
type: "todos/todoAdded",
payload: "Buy milk",
};
action creator
顾名思义创建出一个 action
对象的函数,不需要每次都去手写 action
对象。
const addTodo = (text) => {
return {
type: "todos/todoAdded",
payload: text,
};
};
reducer
前面提到了需要将 action
传入 reducer
进行修改 state
,首先说明 reducer
是一个函数,并且其一定是一个纯函数。函数签名是:(state, action) => newState
。接收当前的 state
和 action
,必要时决定如何更新状态,并返回新状态。
reducer
的原则:
单一数据源:
- 整个
state
被存储在一棵object tree
中,并且这个tree
只能存储一个store
。 Redux
并没有强制让我们不能创建多个store
,但这样不利于数据的维护。- 单一数据源可以让整个应用程序的
state
变得方便维护、追踪、修改。
state
是只读的:
- 唯一修改
state
的方法一定是触发action
,不要试图在其他地方通过任何方式来修改state
。 - 这样就确保了
view
或网络请求都不能直接修改state
,只能通过action
来描述自己想要如何修改state
。 - 保证所有的修改都集中化处理,并且按照严格顺序来执行,不用担心
race condition
的问题。
使用纯函数来执行修改:
- 通过
reducer
将old state
和action
联系起来,并且返回一个新的 state。 - 可以将
reducer
拆分为多个小的reducers
,分别操作不同的state tree
的一部分。 - 但是所有的
reducer
都要是纯函数,不产生副作用。
const initialState = { value: 0 };
function counterReducer(
state = initialState,
action
) {
// 检查 reducer 是否关心这个 action
if (action.type === "counter/increment") {
// 如果是,复制 `state`
return {
...state,
// 使用新值更新 state 副本
value: state.value + 1,
};
}
// 返回原来的 state 不变
return state;
}
store
前面也反复提到了 store
,redux
应用的 state
存在于一个名为 store
的对象中。store
是通过传入一个 reducer
来创建的,并且有一个名为 getState
的方法,返回当前状态值。
import { configureStore } from "@reduxjs/toolkit";
const store = configureStore({
reducer: counterReducer,
});
console.log(store.getState());
// {value: 0}
注意这里用了
toolkit
工具,如果没有使用的话,需要用createStore
,传入reducer
。
dispatch
Redux store
有一个方法叫做 dispatch
,即是更新 state
的唯一方法,调用 store.dispatch()
传入一个 action
对象。store
将执行所有 reducer
函数并计算出更新后的 state
,调用 getState
获取新的 state
。
redux process
- 整个应用程序的
state
通过订阅数据变化 - 将
state
作为props
传入到组件中 - 如果需要改变
state
,通过dispatch
派发预先定义好的action
reducer
接收到action
,根据action
的type
进行更新state
- 最终将
state
更新到整个引用程序的state
react-redux
react-redux
目的就是将单独的 redux
代码融入到 react
中。还是以计数器为例,用类组件来简单说明。核心的代码主要也就两个:
- 在
componentDidMount
中定义数据的变化,当数据变化时,重新设置counter
。 - 在发生事件点击时,调用
store
的dispatch
来派发对应的action
。
simple
下面的 index.js
、reducer.js
、actionCreators.js
表示一整个 store
最简单的配置方式。
- 通过
createStore
创建一个store
。
// index.js
import { createStore } from "redux";
import reducer from "./reducer";
const store = createStore(reducer);
export default store;
- 编写
index.js
中需要的reducer
纯函数。
// reducer.js
import * as actionTypes from "./constants";
const initialState = {
counter: 100,
};
function reducer(state = initialState, action) {
switch (action.type) {
case actionTypes.ADD_NUMBER:
return {
...state,
counter: state.counter + action.num,
};
case actionTypes.SUB_NUMBER:
return {
...state,
counter: state.counter - action.num,
};
default:
return state;
}
}
export default reducer;
- 为了方便创建出
action
,编写actionCreators
。
// actionCreators.js
import * as actionTypes from "./constants";
export const addNumberAction = (num) => ({
type: actionTypes.ADD_NUMBER,
num,
});
export const subNumberAction = (num) => ({
type: actionTypes.SUB_NUMBER,
num,
});
下面具体在 home.jsx
中的使用 store
import React, { PureComponent } from "react";
import store from "../store";
import { addNumberAction } from "../store/actionCreators";
export class Home extends PureComponent {
constructor() {
super();
// 初始化组件的 state
this.state = {
// 获取 store 中的 counter
counter: store.getState().counter,
};
}
componentDidMount() {
// 订阅 store 当 counter 改变时,重新 set 组件内的 state
store.subscribe(() => {
const state = store.getState();
this.setState({ counter: state.counter });
});
}
// 调用 store 的 dispatch 派发任务
addNumber(num) {
store.dispatch(addNumberAction(num));
}
render() {
const { counter } = this.state;
return (
<div>
<h2>Counter: {counter}</h2>
<div>
<button
onClick={(e) => this.addNumber(1)}
>
+1
</button>
</div>
</div>
);
}
}
export default Home;
connect
connect
是一个高阶组件,(mapStateToProps, mapDispatchToProps)
,可以传入两个函数:
- 第一个函数是将
state
映射到组件的props
上,主要是使用state
。 - 第二个函数是将
dispatch
映射到组件的props
上,主要是修改state
。
如下代码,第一个参数是 store
中的 state
,可以直接映射 counter
:
const mapStateToProps = (state) => ({
counter: state.counter,
});
dispatch
会作为第二个函数的第一个参数,可以直接获取到 store.dispatch
派发任务。
const mapDispatchToProps = (dispatch) => ({
addNumber(num) {
dispatch(addNumberAction(num));
},
subNumber(num) {
dispatch(subNumberAction(num));
},
});
因此可以将最上面的 simple
版本代码,修改成如下代码,使用 connect
的高阶组件 Home
。state
和 dispath
都可以在 props
中获取或调度。
export class Home extends PureComponent {
calcNumber(num, isAdd) {
if (isAdd) {
this.props.addNumber(num);
} else {
this.props.subNumber(num);
}
}
render() {
const { counter } = this.props;
return (
<div>
<h2>Counter: {counter}</h2>
<div>
<button
onClick={(e) =>
this.calcNumber(6, true)
}
>
+6
</button>
<button
onClick={(e) =>
this.calcNumber(6, false)
}
>
-6
</button>
</div>
</div>
);
}
}
export default connect(
mapStateToProps,
mapDispatchToProps
)(Home);
后续会使用
useDispatch
和useSelector
在函数组件中使用redux
。可以暂时参考官方文档:The React Counter Component
redux-thunk
上面所有对 counter
的操作都是同步操作 dispatch action
的,state
会被立即更新,但是实际的开发中有大量的操作是需要异步进行的,redux
中也可能有许多从服务器异步拉下来的数据。
在类组件中,一般都只是在 componentDidMount
的生命周期中发送异步请求,然后再等获取到之后通过调用 dispatch action
去改变 store
的 state
。
这样的话逻辑上会有缺陷,网络请求的异步代码需要放在组件的生命周期中完成;其次,网络请求到的数据也属于状态管理的一部分,更好的方式是,让 redux
来进行请求管理。如果想在 redux
中完成异步操作的话,需要引入**中间件(Middleware)**的概念。
Middleware
中间件的目的是在 dispatch
的 action
和最终达到的 reducer
之间,扩展一些自己的代码。而在官网中推荐的、演示的网络请求中间件是 redux-thunk
。
默认情况下我们的 dispatch(action)
,action
是一个普通的 JS
对象,在使用 redux-thunk
后,可以使用 dispatch(actionFunction)
,并且此函数会被调用,调用时候会传入一个 dispatch
函数和一个 getState
函数。
Usage
对于 redux-thunk
的用法,需要 store
时传入应用 middleware
的 enhance
函数。
const enhancer = applyMiddleware(thunk);
const store = createStore(reducer, enhancer);
返回定义一个函数的 action
,这里已经不再是对象,dispatch
派发的是一个函数,该函数会在 dispatch
之后执行。
// actionCreators.js
export const fetchDataAction = () => {
return (dispatch, getState) => {
axios
.get("https://localhost:7777/get")
.then((res) => {
dispatch(changeDataAction(res.data));
});
};
};
回到类组件中,还是使用 connect
将 dispatch
作为 props
传入到类组件中,后续的流程和前面的保持一致,在 componentDidMount
中调用 this.props.fetchData()
获取数据,并且保存到 store
中。
const mapDispatchToProps = (dispatch) => ({
fetchData() {
dispatch(fetchDataAction());
},
});
reducer code splitting
之前提到过,通过 createStore
创建 store
时,需要传入一个 reducer
,但是针对不同的模块进行一起管理会很混乱,这个时候引入了 reducer
的代码分块,将一个大的 reducer
分为许多小模块的 reducer
,通过 combineReducers API
进行组合起来,再通过 createStore
创建 store
。
const reducer = combineReducers({
counter: counterReducer,
home: homeReducer,
user: userReducer,
});
const store = createStore(reducer, enhancer);
对于 combineReducers
的实现原理,本质上是将我们传入的 reducers
合并到一个对象,最终返回一个 combination
函数。执行 combination
之前会先判断前后返回的数据是否相同来决定返回之前的 state
还是新的 state
。新的 state
会触发订阅者发生对应的刷新,旧的 state
可以有效的阻止订阅者发生刷新。
function reducer(state = {}, action) {
return {
counter: counterReducer(
state.counter,
action
),
home: homeReducer(state.home, action),
user: userReducer(state.user, action),
};
}
如果这里这么用了之后,前面所再类组件中使用 store
的 state
需要注意,这时应该走到对应模块的 reducer
下面,再获取 state
。
redux toolkit
如果在前面是用 vscode
进行上述实验,会发现 createStore
这个 API
已经不推荐使用,这是因为官方推荐使用 rtk
进行相关逻辑的编写。虽然前面也可以通过代码拆分、文件拆分达到分模块管理,但是代码量过多,不利于管理。
在 rtk
中有几个核心的 API
:
configureStore
:对前面的createStore
进行了简化配置选项,进行包装。可以自动组合我们的slice reducer
,添加提供的任务redux middleware
,并且默认包含了redux-thunk
和redux devtools extension
。createSlice
:顾名思义创建一个切片,接收reducer
函数的对象、切片名称和初始状态值,并且自动生成切片reducer
,带有响应的actions
。createAsyncThunk
:接受一个动作类型字符串和一个返回承诺的函数,并生成一个pending
、fulfilled
、rejected
分派动作类型的thunk
。
在底层,rtk
使用 immerJS
库,保证了数据的不可变性。会跟踪我们尝试进行的所有更改,然后使用该更改列表返回一个安全的、不可变的更新值,就好像我们手动编写了所有不可变的更新逻辑一样。
You can only write “mutating” logic in Redux Toolkit’s createSlice and createReducer because they use Immer inside! If you write mutating logic in reducers without Immer, it will mutate the state and cause bugs!
refactor
下面使用 createSlice
重构上面不同模块的 reducer
。其包含如下几个参数:
name
:用户标记slice
的名词。在之后的redux-devtool
中会显示对应的名词。initalState
:初始化值,第一次初始化的state
。reducers
:相当于之前的reducer
函数,对象类型,并且可以添加很多的函数,每个函数类似于之前reducer
中的case
。其包括两个参数:state
。- 调用这个
action
时,传递的action
参数。
重构上面计数器案例。
import { createSlice } from "@reduxjs/toolkit";
const counterSlice = createSlice({
name: "counter",
initialState: {
counter: 888,
},
reducers: {
addNumber(state, { payload }) {
state.counter = state.counter + payload;
},
subNumber(state, { payload }) {
state.counter = state.counter - payload;
},
},
});
export const { addNumber, subNumber } =
counterSlice.actions;
export default counterSlice.reducer;
当使用 createSlice
重构之前的 reducer
之后,使用 configureStore
创建 store
。
import { configureStore } from "@reduxjs/toolkit";
import counterReducer from "./features/counter";
import homeReducer from "./features/home";
const store = configureStore({
reducer: {
counter: counterReducer,
home: homeReducer,
},
});
export default store;
aysnc
在之前的内容中可以通过 redux-thunk middleware
让 dispatch
进行异步操作。在 rtk
中,通过 createAsyncThunk
进行异步操作。
export const fetchHomeMultidataAction =
createAsyncThunk(
"fetch/homemultidata",
async (payload, extraInfo) => {
const res = await axios.get(
"https://localhost:7777/get"
);
return res.data;
}
);
在 createSlice
的 extraReducer
中监听结果
extraReducers: {
[fetchHomeMultidataAction.pending](state, action) {
console.log("fetchHomeMultidataAction pending");
},
[fetchHomeMultidataAction.fulfilled](state, { payload }) {
state.banners = payload.data.banner.list;
state.recommends = payload.data.recommend.list;
},
[fetchHomeMultidataAction.rejected](state, action) {
console.log("fetchHomeMultidataAction rejected");
},
},
当然也可以直接在 createAsyncThunk
中进行 dispatch
派发。
export const fetchHomeMultidataAction =
createAsyncThunk(
"fetch/homemultidata",
async (extraInfo, { dispatch, getState }) => {
// 1.发送网络请求, 获取数据
const res = await axios.get(
"https://localhost:7777/get"
);
// 2.取出数据, 并且在此处直接dispatch操作(可以不做)
const banners = res.data.data.banner.list;
const recommends =
res.data.data.recommend.list;
dispatch(changeBanners(banners));
dispatch(changeRecommends(recommends));
// 3.返回结果, 那么action状态会变成fulfilled状态
return res.data;
}
);
custom connect
前面通过引入 react-redux
的 connect
实现将 store
的 state
和 dispatch
映射到类组件的 props
上,从用法上来看,本质上返回的就是一个高阶组件。下面简单实现一下 connect
。
export default connect(
mapStateToProps,
mapDispatchToProps
)(About);
从这里看来,connect
需要传入两个参数,并且返回一个函数,返回的函数可以传入类组件,因此不难写出框架。
export function connect(
mapStateToProps,
mapDispatchToProps
) {
return function (WrapperComponent) {};
}
然后对传入进来的 WrapperComponent
进行二次包装之后返回出去。其次就是需要处理 connect
的核心逻辑,connect
主要就是将 state
和 dispatch
挂到新类组件的属性上去,让它可以通过 props
能够访问。
code
export function connect(
mapStateToProps,
mapDispatchToProps
) {
return function (WrapperComponent) {
class NewComponent extends PureComponent {
constructor(props) {
super(props);
this.state = mapStateToProps(
store.getState()
);
}
componentDidMount() {
this.unsubscribe = store.subscribe(() => {
this.setState(
mapStateToProps(store.getState())
);
});
}
componentWillUnmount() {
this.unsubscribe();
}
render() {
const stateObj = mapStateToProps(
store.getState()
);
const dispatchObj = mapDispatchToProps(
store.dispatch
);
return (
<WrapperComponent
{...this.props}
{...stateObj}
{...dispatchObj}
/>
);
}
}
return NewComponent;
};
}
通过上面的代码实现了 connect
的核心功能,但是比较依赖于导入的 store
。如果将其封装成一个独立的库,需要依赖与创建的 store
。因此需要对代码进行改进。
optimization code
React 官方文档:
因此可以提供一个 Provider
,来源于创建的 Context
,让用户将 store
传入到value
。这样可以在类组件中通过 context
在 connect
中获取到 store
。优化后的代码如下所示。
export function connect(
mapStateToProps,
mapDispatchToProps
) {
return function (WrapperComponent) {
class NewComponent extends PureComponent {
constructor(props, context) {
super(props);
this.state = mapStateToProps(
context.getState()
);
}
componentDidMount() {
this.unsubscribe = this.context.subscribe(
() => {
this.setState(
mapStateToProps(
this.context.getState()
)
);
}
);
}
componentWillUnmount() {
this.unsubscribe();
}
render() {
const stateObj = mapStateToProps(
this.context.getState()
);
const dispatchObj = mapDispatchToProps(
this.context.dispatch
);
return (
<WrapperComponent
{...this.props}
{...stateObj}
{...dispatchObj}
/>
);
}
}
NewComponent.contextType = StoreContext;
return NewComponent;
};
}
在 index.js
中,需要提供 value
。
// index.js
root.render(
<React.StrictMode>
<Provider store={store}>
<StoreContext.Provider value={store}>
<App />
</StoreContext.Provider>
</Provider>
</React.StrictMode>
);
custom middleware
在使用 redux-thunk
时,提到了 middleware
的概念,在很多场景下,可能需要我们来自定义中间件,因此这里以自定义一些简单的中间件为例。
const store = createStore(reducer, enhancer);
在没有使用 rtk
时,通过 createStore
来创建 store
,将使用的中间件作为第二个参数 enhancer
传递进去。因此我们在构建创建 store
时,就不采用传递第二个参数。
以自定义一个 thunk
为例,thunk
是在 store
派发(dispatch
)时,可以传入一个函数,并且会执行该函数之后再进行派发。
- 先创建一个
thunk
函数,将store
作为参数传递进去。 - 先保存原本的
dispatch
内容到next
。 - 修改
dispatch
的指向,让它指向我们自定义的dispatchThunk
函数。 - 在
dispatchThunk
中,需要判断action
是函数或者是对象。 4.1. 如果是对象,直接使用原本的dispatch
派发下去。 4.2. 如果是函数,需要执行这个函数。
function thunk(store) {
const next = store.dispatch;
function dispatchThunk(action) {
if (typeof action === "function") {
action(store.dispatch, store.getState);
} else {
next(action);
}
}
store.dispatch = dispatchThunk;
}
export default thunk;
在 store/index.js
创建 store
后,只需要对 thunk
进行调用,thunk(store)
即可实现对自定义中间件的使用。
use hooks
在 react-redux
中提供了两个 hooks
函数,useSelector
和 useDispatch
,从命名角度不难看出它想表达的意思。useSelector
主要用于选择需要的 store
,useDispatch
主要是用于派发任务。
下面将使用 React Hooks
来体验一下 redux
。对于创建 store
这里使用 rtk
来创建,步骤和前面所写到的内容是一致的。这里只说明一下函数组件中如何使用 store
。
import { useState } from "react";
import {
useSelector,
useDispatch,
} from "react-redux";
import {
selectCounter,
addNumber,
subNumber,
addNumberAsync,
} from "../store/features/counter/counterSlice";
export const Counter = () => {
const count = useSelector(selectCounter);
const dispatch = useDispatch();
const [increment, setIncrement] = useState(8);
return (
<div>
<div className="store">
store counter: {count}
</div>
<div className="increment">
increment / decrement: {increment}
</div>
<div>
<input
type="text"
value={increment}
onChange={(e) =>
setIncrement(e.target.value)
}
/>
</div>
<div className="operator">
<button
onClick={() =>
dispatch(addNumber(Number(increment)))
}
>
Add (+)
</button>
<button
onClick={() =>
dispatch(subNumber(Number(increment)))
}
>
Sub (-)
</button>
<button
className="asyncButton"
onClick={() =>
dispatch(
addNumberAsync(Number(increment))
)
}
>
Add Async
</button>
</div>
</div>
);
};
summary
本篇文章记录了 redux
的一些简单逻辑、使用方法。并且最后和朋友讨论了下redux
确实概念多,使用繁琐,不过学习完能够了解到 react
相关生态。在不同的公司不同部门对状态管理的选择都是不同的,有的使用 redux
,有的使用 recoil
,甚至有些厉害的部门会使用自研的状态管理工具,所以学习 redux
只是学习这个思想,再熟悉之后,自己能够开发出适合自己的状态管理库。如果后续有时间的话,可以尝试自己写一个状态管理库。