TypeScript+Redux 优化

mapStateToProps && mapDispatchToProps

如果组件需要从 store 中获取一些数据或者方法经常会这么写:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import { TRepo } from "store/repos/types";

interface RepoProps {
repos: TRepo[];
username: string;
push: Function;
fetchRepos: Function;
}
class Repos extends React.Component<RepoProps, {}> {}

const mapStateToProps = ({ repos, router }: ApplicationState) => ({
repos: repos.repos,
username: queryString.parse(router.location.search).username
});

const mapDispatchToProps = (dispatch, getState) => {
return {
push: url => dispatch(push(url)),
fetchRepos: value => dispatch(reposActions.fetchReposByUser(value))
};
};

上面的这种写法有一个问题,需要人工维护 mapStateToProps 和 mapDisPatchToProps 与 RepoProps 的统一,如果 mapStateToProps 返回更多数据,RepoProps 就需要跟着修改。这里可以利用 ReturnType 减少不必要的重复

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// store/index.ts
export type TConnectType<
S extends (...args: any[]) => any,
D extends (...args: any[]) => any
> = ReturnType<S> & ReturnType<D>;

// components/Repos/components.tsx
import { ApplicationState, TConnectType } from "store";

export interface ReposProps {}

type AllProps = ReposProps &
TConnectType<typeof mapStateToProps, typeof mapDispatchToProps>;

class Repos extends React.Component<AllProps, any> {}

typesafe

让 Redux 实现 typesafe,可以使用tyepsafe-actions

具体使用这里就不介绍了。

简化 switch case

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
switch (actin.type) {
case getType(globalActions.getVersionSuccess):
return {
...state,
version: action.payload
};

case getType(globalActions.toggleLogin):
return {
...state,
showLogin: action.payload
};

case getType(globalActions.getUserLoginWaySuccess):
return {
...state,
loginWay: action.payload
};
}

以上 case 很常见:

1
key: action.payload;

但是却很繁琐,逻辑重复。我们来封装一个函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
function handleInit<T extends {}, K extends keyof T>(
state: T,
stateKey: K,
action
): T {
return { ...state, [stateKey]: action.payload };
}
export function handleAll<T>(state: T, action, handle = handleInit): T {
if (action.stateKey) {
return handle(state, action.stateKey, action);
}
return state;
}

使用如下:

1
2
3
4
5
6
7
8
9
10
dispatch({
type: "GET_VERSION",
stateKey: "version"
});

// reducer
switch (actin.type) {
default:
return handleAll(state, action);
}

action

使用 typesafe-actions 来创建一个 action 如下:

1
2
3
4
5
6
7
// {
// type: GlobalTypes.global_VERSION.success,
// payload: string
// }
export const getVersionSuccess = createStandardAction(
GlobalTypes.global_VERSION.success
)<string>();

如果想使用 handle 来简化 case,需要在 action 中添加 stateKey:

1
2
3
4
5
{
type: 'any_type',
payload: 'v1.2',
stateKey: 'version'
}

我们封装下 createStandardAction 来支持 stateKey:

1
2
3
4
5
6
7
8
9
10
11
12
/**
* @desc 生成带有 stateKey 的 action
* @param type - action.type
*/
export function createStandardAction2<T extends string>(type: T) {
return function inner<P, SAll>(stateKey: keyof SAll) {
return createStandardAction(type).map((payload: P) => ({
payload,
stateKey
}));
};
}

现在我们来重写:

1
2
3
4
5
6
7
type GlobalState = {
version: string;
other: string;
};
export const getVersionSuccess = createStandardAction2(
GlobalTypes.global_VERSION.success
)<string, GlobalState>("versin");

简化 action type 创建

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
export enum GlobalTypes {
IS_CLUSTER = "global/IS_CLUSTER",
GET_USER_INFO = "global/GET_USER_INFO",
GET_USER_INFO_SUCCESS = "global/GET_USER_INFO_SUCCESS",
GET_USER_INFO_ERROR = "global/GET_USER_INFO_ERROR",
LOGOUT = "global/LOGOUT",
USER_INSTALL_WAY = "global/USER_INSTALL_WAY",
USER_INSTALL_WAY_SUCCESS = "global/USER_INSTALL_WAY_SUCCESS",
USER_INSTALL_WAY_ERROR = "global/USER_INSTALL_WAY_ERROR",
VERSION = "global/VERSION",
VERSION_SUCCESS = "global/VERSION_SUCCESS",
VERSION_ERROR = "global/VERSION_ERROR",
LOGIN_WAY = "global/LOGIN_WAY",
LOGIN_WAY_SUCCESS = "global/LOGIN_WAY_SUCCESS",
LOGIN_WAY_ERROR = "global/LOGIN_WAY_ERROR",
TOGGLE_LOGIN = "global/TOGGLE_LOGIN",
GET_INSIGHT_URL = "global/GET_INSIGHT_URL",
GET_INSIGHT_URL_SUCCESS = "global/GET_INSIGHT_URL_SUCCESS",
GET_INSIGHT_URL_ERROR = "global/GET_INSIGHT_URL_ERROR",
GET_URLS = "global/GET_URLS",
GET_URLS_SUCCESS = "global/GET_URLS_SUCCESS",
GET_URLS_ERROR = "global/GET_URLS_ERROR",
ADMIN_USER = "global/ADMIN_USER"
}

简化后:

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
enum BasicTypes {
global_IS_CLUSTER,
global_TOGGLE_LOGIN,
global_ADMIN_USER
}

enum FetchTypes {
global_GET_USER_INFO,
global_LOGOUT,
global_USER_INSTALL_WAY,
global_VERSION,
global_LOGIN_WAY,
global_GET_INSIGHT_URL,
global_GET_URLS
}

export type TFetchTypes = typeof FetchTypes;

// {
// global_IS_CLUSTER: 'global_IS_CLUSTER',
// global_TOGGLE_LOGIN: 'global_TOGGLE_LOGIN',
// global_ADMIN_USER: 'global_ADMIN_USER',
// global_GET_USER_INFO: {
// request: 'global_GET_USER_INFO_REQUEST',
// success: 'global_GET_USER_INFO_SUCCESS',
// error: 'global_GET_USER_INFO_ERROR',
// }
// }
export const GlobalTypes = composeTypes({
BasicTypes,
FetchTypes
});

简化异步 action

一个异步 action 通常需要创建三个不同的 action

  1. request
  2. success
  3. failure ( error )

typesafe-actions 已经为我们封装了创建异步 action 集合的方法:

1
2
3
4
5
6
7
const actions = createAsyncAction(
requestType, successType, failureType, cancelType?
)<TRequestPayload, TSuccessPayload, TFailurePayload, TCancelPayload?>()

actions.request(TRequestPayload) // {type: requestType, payload: TRequestPayload}
actions.success(TSuccessPayload) // {type: successType, payload: TSuccessPayload}
actions.failure(TFailurePayload) // {type: failureType, payload: TFailurePayload}

简化前:

1
2
3
4
5
6
7
export const getUrls = createAction(GlobalTypes.global_GET_URLS.request);
export const getUrlsSuccess = createStandardAction(
GlobalTypes.global_GET_URLS.success
)<TUrls>();
export const getUrlsFailure = createStandardAction(
GlobalTypes.global_GET_URLS.error
)<string>();

简化后:

1
2
3
4
5
const actions = createAsyncAction(
GlobalTypes.global_GET_URLS.request,
GlobalTypes.global_GET_URLS.success,
GlobalTypes.global_GET_URLS.error
)<undefined, TUrls, string>();

可以看到,创建异步 action 的步骤已经极大简化。但是,上边的 action type 还是略重复,为了去除重复我们包装一下 createAsyncAction:

1
2
3
4
5
6
7
8
9
10
11
12
type TAsyncTypes<K extends string> = {
request: "loading";
success: K;
error: "error";
};

export function createAsyncAction2<T extends string>(types: TAsyncTypes<T>) {
const { request, success, error } = types;
return function inner<T1, T2, T3 = Error, T4 = string>() {
return createAsyncAction(request, success, error)<T1, T2, T3, T4>();
};
}

使用 createAsyncAction2 之后,生成异步 actions 集合方法就非常简单了:

1
2
3
4
5
const actions = createAsyncAction(GlobalTypes.global_GET_URLS)<
undefined,
TUrls,
string
>();

简化 loading

loading 的处理对于 redux 来说比较繁琐,这里我单独写了一篇文章:

TypeScript Redux 处理loading

总结

使用 Redux 是比较繁琐的,但是可以通过进一步封装来简化代码,加上 Typescript 加持,编写高质量代码更加容易。