TypeScript Redux 处理loading

Redux 中处理 loading 是比较繁琐的,经常会出现重复代码:

1
2
3
4
5
6
7
8
9
10
11
12
switch (action.type) {
case "getInfo":
return {
...state,
infoLoading: true
};
case "getInfoSuccess":
return {
...state,
infoLoading: false
};
}

重构 1

我们可以考虑建一个单独的 loading reducer,把所有有关 loading 的字段放到一块处理:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
// globalLoadingReducer.ts
export const types = {
GLOBAL_LOADING: "globalLoading/GLOBAL_LOADING"
};

/** *** reducer *** */
const initialState = {};

function globalLoading(state = initialState, action) {
const { name, value } = action;
switch (action.type) {
case types.GLOBAL_LOADING:
if (Array.isArray(name)) {
const res = name.reduce((ob, name) => {
ob[name] = false;
return ob;
}, {});
return {
...state,
...res
};
}
return {
...state,
[name]: value
};

default:
return state;
}
}

export default globalLoading;

调用如下:

1
2
3
4
5
6
7
8
9
10
setLoading(["userInfo", "list"], true);

setLoading(["userInfo", "list"], false)

functoin mapStateToProps(state){
return {
userInfoLoading: state.globalLoading.userInfo,
listLoading: state.globalLoading.list
}
}

这个方法虽然集合了所有的 loading,但是缺点很多:

  1. 需要手动调用开始和结束 loading 状态。
  2. ts 无法检测到 loading 关键字,如 globalLoading 不知道自己有 userInfo,除非你手动加进 globalLoading 类型中。

重构 2

同样将 loading 提取到一个 reducer 来处理,不同的是这次使用一定的规则来编写 action.type:

  • 带有 _{typeName}_REQUEST_ 的 type 表示异步请求开始,例如 GET_USER_INFO_REQUEST
  • 带有 _{typeName}_SUCCESS_ 的 type 表示异步请求成功,例如 GET_USER_INFO_SUCCESS
  • 带有 _{typeName}_ERROR_ 的 type 表示异步请求开始,例如 GET_USER_INFO_ERROR

通过正则检测 type,可以确定 type 的名字和状态:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// globalLoadingReducer.js

import { GlobalLoadingState } from "./types";

const initialState: GlobalLoadingState = {};

const reducer: Reducer<GlobalLoadingState> = (state = initialState, action) => {
const { type } = action;
const matches = /(.*)_(REQUEST|SUCCESS|ERROR)/.exec(type);

// not *_REQUEST / *_SUCCESS / *_FAILURE actions, 忽略
if (!matches) return state;

const [, requestName, requestState] = matches;
return {
...state,
[requestName]: requestState === "REQUEST"
};
};

使用如下:

1
2
3
4
// getUser loading start
dispatch("getUser_REQUEST");
// getUser loading end
dispatch("getUser_SUCCESS");

一个 store 中其实包含两种 action,一种普通的 action,另一种需要异步处理的 action:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// store/global/types

// 普通action
enum BasicTypes {
global_ADMIN_USER
}

// 需要发送请求的action
enum FetchTypes {
global_GET_USER_INFO
}

export type TFetchTypes = typeof FetchTypes;

export const GlobalTypes = composeTypes({
BasicTypes,
FetchTypes
});

// GlobalTypes 形式:
// GlobalTypes = {
// global_ADMIN_USER: "global_ADMIN_USER",
// global_GET_USER_INFO: {
// request: "loading",
// success: "global_GET_USER_INFO",
// error: "error"
// }
// };

上边拆分了普通 action 和需要异步的 action,我们把这些异步的 action 类型导入到全局的 loading 类型中:

1
2
3
4
5
6
7
8
9
10
// ./types.ts

import { TFetchTypes } from "store/global/types";

// 导入每个action中的FetchTypes,方便确定loading属性
type GlobalLoadingKeys = keyof TFetchTypes;

export type GlobalLoadingState = Partial<
{ [key in GlobalLoadingKeys]: boolean }
>;

这样,ts 就会提示 loading 属性了:

1
2
3
const mapStateToProps = (state: ApplicationState) => ({
userLoading: state.globalLoading.global_GET_USER_INFO
});