一、前言
在一般的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;
七、最后
写的比较乱,当做学习笔记写的,后面有时间会持续进行优化和补充,如果有错误,感谢指正!