一、前言
在一般的React项目中,大多数情况会选择Redux或者React-Redux作为我们的状态管理工具,在使用Vuex的时候,我们可以在mutations里写同步操作,也可以在actions里写异步操作,然而Redux不同于Vuex,Redux本身是不支持异步的,如果需要处理异步操作,我们还要额外安装redux-thunk或者redux-saga这样的中间件,显得很繁琐。
如果你的React项目中使用了react hook、redux、redux-thunk、或者redux-saga,那么可能你需要用redux-toolkit(以下简称RTK)来优化你的项目结构,它可以让你的代码看起来更清爽。
# 优化前
/counter
constants.ts
actions.ts
reducer.ts
saga.ts
index.tsx
# 优化后
/counter
slice.ts
index.tsx
二、简介
RTK旨在帮助解决关于Redux的几个问题:
- 配置复杂,devtool...
- 模板代码太多,创建constant,action,reducer...
- 需要添加很多依赖包,如redux-thunk、redux-saga、immer...
简单讲,配置Redux的流程太过复杂,完整需要编写actionTypes、actions、reducer、store等一系列函数,最后通过connect隐射到props里面供组件使用。而使用RTK,只需一个reducer即可。
那么RTK到底是什么呢?
三、核心依赖
我们打开GitHub,找到RTK的源码,可以在 toolkit/package.json目录中看到关于RTK用到的一些依赖:
关于这些依赖都代表什么呢?
第一个: redux
当然了,RTK需要它才能工作,这里引入了redux,意味着如果我们的项目安装了RTK,就不需要重复安装redux;除此之外,如果大家观察的足够仔细,就会发现RTK的依赖并不包含react-redux,这意味着,RTK是一个独立的库,它不只是给react项目使用,如果你愿意,你可以在任何环境中使用它,比如Vue或者Angular,甚至jQuery中,或者原生js中都可以使用。
第二个: redux-thunk
RTK自带了redux-thunk来处理异步逻辑,thunk在RTK中是默认开启的(在开发过程中,你可以手动关闭,如果你愿意,也可以安装redux-saga等其它异步处理的中间件)。
第三个: reselect
这也是一个比较流行的redux插件,它可以帮助我们在视图渲染的时候记住当前的状态,防止组件在不需要的时候被无意识的渲染,功能有点类似于shouldComponentDidUpdate,但他们并不是一个东西,这里作为RTK入门笔记,对该插件不做深入讲解。
第四个: immer
最后一个immer是个非常有意思的插件,它允许我们把state的immutable特性转化为mutable,也就是说,我们在reducer函数中,可以直接修改state中的数据(我在第一次听到这个思路的时候,理智告诉我,这是不对的,因为这样就违反了reducer函数式编程的理念,redux是单向数据流的状态管理工具,数据是不可变的,我们不可以直接修改store的状态,而是通过返回新state,替换旧state来完成状态更新,所以我一开始接触到这个概念的时候,还是比较抵触的)。总而言之,我们可以选择使用immer来修改state,让数据变成mutable状态,也可以选择不使用immer,让数据保持immutable状态,这完全取决于你自己,大家可以自行决定。(这不正是React的灵活之处吗)
四、关于immer库
在上文中,我们对immer做了简单的介绍,这里再单独拿出来讨论一下
immer到底是什么?在上文中我们提到,它是一个很有意思的插件,它允许我们把state的immutable特性转化为mutable,实际上,immer在底层是的核心实现是利用了ES6的proxy,在我们对状态进行修改的时候,proxy对象进行拦截,并且proxy按顺序替换上层对象,相当于自动帮你返回的新对象(所以其实还是immutable的,只不过写法上看起来是可直接修改state)
下面的例子是用immer和不用immer的区别:
// 不使用 immer,返回新状态,替换旧状态
reducers: {
fetchStart(state) {
return { ...state, loading: true };
},
fetchEnd(state) {
return { ...state, loading: false };
},
fetchSuccess(state, action: PayloadAction<FetchType>) {
return { ...state, data: action.data };
},
fetchFailure(state, action: PayloadAction<ErrorType>) {
return { ...state, error: action.message };
},
},
// 使用 immer,无需返回新状态,直接修改原状态
reducers: {
fetchStart(state) {
state.loading = true;
},
fetchEnd(state) {
state.loading = false;
},
fetchSuccess(state, action: PayloadAction<FetchType>) {
state.data = action.data;
},
fetchFailure(state, action: PayloadAction<ErrorType>) {
state.error = action.message
},
},
于是有同学会问了,在上面的例子中,使用immer和不使用immer的代码行数是一样的,也没体现出代码的简化,所以它的优点体现在哪里呢?
再看下面的例子:
// 不使用 immer,返回新状态,替换旧状态
reducers: {
someReducer(state, action: PayloadAction<SourceData>) {
return {
...state,
first: {
...state.first,
second: {
...state.first.second,
third: {
...state.first.second.third,
value: action.someValue,
},
},
},
};
},
},
// 使用 immer,无需返回新状态,直接修改原状态
reducers: {
someReducer(state, action: PayloadAction<SourceData>) {
state.first.second.third.value = action.someValue;
},
},
这是一个很典型的例子,如果我们的状态嵌套了很多层,并且需要修改的数据在很深层,这时immer的便利性就体现出来了,主要有以下两个好处:
- 写法大大简化,一处逻辑一行代码即可实现
- 对象嵌套很深的时候,手动编写
immutable update的逻辑是很困难的,并且用户在reducer中修改状态时,可能会因为粗心而犯错,启用immer可以很好的规避这一点
五、核心api
-
configureStore
这是对标准Redux中createStore函数的封装,它为store添加了一些配置,以获得更好的开发体验,包裹createStore,并集成了redux-thunk、Redux DevTools,默认开启
-
getDefaultMiddleware
返回一个包含默认middleware的数组,默认情况下,configureStore会自动添加一些中间件到store设置中。如果你想自定义middleware列表,你可以将自己的middleware添加到getDefaultMiddleware返回的数组中。
-
createReducer
简化了标准Redux中的reducer函数。内置了immer(默认开启),通过在reducer中编写mutable代码,极大简化了immutable的更新逻辑(上文中有详细介绍过immer库),除此之外,在RTK中使用createReducer函数创建reducer的时候,有两中创建方式,一种回调函数的方式,一种映射对象的方式。(我更喜欢后者,因为映射对象的书写方式看起来更加直观、更加容易理解)
-
createAction
用于创造和定义标准Redux类型的函数。传入一个常量类型,它会返回一个携带payload的函数,和标准redux中的action基本类似。
-
createSlice
这个createSlice函数,在我看来是RTK中的核心api,官方文档中对它的描述是这样的:该函数接收一个初始化state对象,和一个reducer对象,它可以将store以slice的方式分割成为不同的部分,每个部分都会独立生成相对应的action和state对象。在99%的情况下,我们都不会直接使用createReducer和createAction,取而代之的就是createSlice。
-
createAsyncThunk
用来处理异步操作的方法,对于使用RTK的项目来说,完成异步操作主要分三个步骤,createAsyncThunk方法主要用来创建异步函数,创建完毕之后在reduce中进行处理,最后在业务代码中用dispatch进行调用,基本流程和标准的Redux并无二致。(需要注意的是,在createSlice中,我们不可以用普通的reduce处理异步函数,必须使用 extraReducers来处理异步)
六、如何使用
1. 安装
# 使用 npm
npm install @reduxjs/toolkit
# 使用 yarn
yarn add @reduxjs/toolkit
2. 初始化state
interface InitialState {
count: number;
}
const initialState: InitialState = {
count: 0,
};
3. 创建slice
export const getData = createSlice({
name: "nameSpace",
initialState,
reducers: {},
extraReducers: {},
});
4. 创建异步函数
const fetchData = createAsyncThunk("nameSpace/fetchData", async () => await axios(someAPI));
5. 传入异步函数,更新状态
const getData = createSlice({
name: "nameSpace",
initialState,
reducers: {},
extraReducers: {
[fetchData.fulfilled.type]: (state: InitialState, action: PayloadAction<InitialState>) => {
state.count = action.payload.count;
},
},
});
6. 创建Reducer
const rootReducer = combineReducers({ data: getData.reducer });
7. 创建仓库
const store = configureStore({
reducer: rootReducer,
middleware: getDefaultMiddleware => [...getDefaultMiddleware()],
devTools: true,
});
export default store;
完整代码如下:
import { createSlice, PayloadAction, createAsyncThunk, combineReducers, configureStore } from "@reduxjs/toolkit";
import axios from "axios";
interface InitialState {
count: number;
}
const initialState: InitialState = {
count: 0,
};
export const fetchData = createAsyncThunk("nameSpace/fetchData", async () => await axios(someAPI));
export const getData = createSlice({
name: "nameSpace",
initialState,
reducers: {},
extraReducers: {
[fetchData.fulfilled.type]: (state: InitialState, action: PayloadAction<InitialState>) => {
state.count = action.payload.count;
},
},
});
const rootReducer = combineReducers({ data: getData.reducer });
const store = configureStore({
reducer: rootReducer,
middleware: getDefaultMiddleware => [...getDefaultMiddleware()],
devTools: true,
});
export default store;
8. 在业务代码中使用:
import React, { useEffect } from "react";
import { useDispatch } from "react-redux";
import { fetchData } from "../redux/slice";
import { useSelector } from "../redux/hooks";
const App: React.FC = () => {
const dispatch = useDispatch();
const count = useSelector(({ data }) => data);
useEffect(() => {
dispatch(fetchData());
}, []);
return <div>{count}</div>;
};
export default App;
七、最后
写的比较乱,当做学习笔记写的,后面有时间会持续进行优化和补充,如果有错误,感谢指正!