@tkit/*
是内部版本,替换成 tkit-*
则是对外版本react
18 年 9 月的时候,咱们团队开始全面推广 TypeScript + React + Redux 的方案,其中 Redux 这块方案选型:redux
redux-actions
建立 action 和 reducerredux-saga
处理反作用一个类型化后的 redux action 代码示例以下:数据结构
// store
interface State { ... }
const initialState: State = { ... };
// action
const DOSOMETHING = 'DOSOMETHING';
const doSomething = createAtion(DOSOMETHING, (id: number) =>({ id }));
function* doSomethingEffect(action: Action<{ id: number }>) { ... };
function* doSomethingSaga() {
yield takeEvery(DOSOMETHING, doSomethingEffect);
}
// reducer
const applySomething = handleActions({
[DOSOMETHING]: (state: IInitialState, action: Action<{ id: number }>) => { ... }
});
复制代码
以上的方案,随后就经历第一个大型项目的拷打,上百个的这样的 redux action ——即使引入了 redux-actions
来简化建立 action 和 reducer 的编码,依旧痛点满满:app
代码结构过于繁复,再加上类型注解,开发体验指数级下降async
后来者的优点是总有别人的经验可以参考,大型实践以后,咱们引入了 dvajs 相似的数据结构来管理 react redux:函数
interface Model<S extends any> {
state: S;
reducers: {
[doSomething: string]: (state: S, action: Action<any>) => S;
};
effects: {
[doSomethingAsync: string]: (action: Action<any>, utils: SagasUtils) => Iterator<{}, any, any>;
};
}
复制代码
同时,总结了这个结构的不足:ui
bindActionCreators
贯通对于不足 1——调整 effect 参数顺序便可:编码
interface Model<S extends any> {
...
effects: {
[doSomethingAsync: string]: (utils: SagasUtils, action: Action<any>) => Iterator<{}, any, any>;
};
...
}
复制代码
而其余不足,则须要另辟蹊径spa
单个 effect 内部的类型化以及 action 参数的贯通,设计到类型计算,仅仅经过 interface 泛型是作不到,咱们须要一个工厂函数——来实现参数类型对返回类型的复杂逻辑关系。设计
这个工厂函数的基本结构:
interface CreateModel {
<S, R, E>
(model: {
state: S;
reducers: R;
effects?: E;
}
): {
state: S;
reducers: ApplySomething<S, R>;
actions: DoSomethings<R, E>
}
}
复制代码
export interface Reducers<S> {
[doSomething: string]: <P extends AbstractAction>(state: Readonly<S>>, action: P) => S;
}
interface CreateModel {
<S, R extends Reducers<S>, ...>(model: {
state: S;
reducers: R;
...
}): any
}
复制代码
在 effect 逻辑里直接调用 model.actions.doSomething——须要显式指定 effect 返回值类型,不然将陷入类型推断循环引用。
const model = createModel({
...
effects: {
*doSometingAsync(...): Iterator<{}, any, any> {
model.actions.doSomething(action)
}
}
...
})
复制代码
bindActionCreators
类型贯通思路在于将 reducers 和 effects 内各方法 的 action 参数类型提取出来,用以推断工厂函数返回 model actions 属性的类型:
interface EffectWithPayload<Utils extends BaseEffectsUtils> {
<P extends AbstractAction>(
saga: Utils,
action: P
) => Iterator<{}>
}
interface Effects<Utils extends BaseEffectsUtils> {
[doSomethingAsync: string]: EffectWithPayload<Utils>
}
interface CreateModel {
<S, R extends Reducers<S>, E extends Effects<SagaUtils>>(model: {
state: S;
reducers: R;
effects: E;
...
}): {
...
actions: {
[doSomething in keyof R]: <A extends Parameters<R[doSomething]>>[1]>(payload: A['payload']) = > A;
[doSomethingAsync in keyof E]: <A extends Parameters<E[doSomethingAsync]>>[1]>(payload: A['payload']) = > XXX;
}
}
}
复制代码
实现了 model.actions 自动类型推断,经过 bindActionCreators
connect 到组件时,各个 action 也是类型化的:
function Demo(props: { actions: typeof model.actions }) {
// ok
props.actions.doSomething(paramsMathed);
// TS check error
props.actions.doSomething(paramsMismatched);
...
}
复制代码
首先,类型定义上扩充 effect 泛型:
interface HooksModelEffectWithPayload<Utils extends BaseEffectsUtils> {
<P extends AbstractAction>(saga: Utils, action: P): Promise<any>;
}
interface ReduxModelEffects {
[doSomethingAsync: string]:
| EffectWithPayload<ReduxModelEffectsUtils>
| HooksModelEffectWithPayload<ReduxModelEffectsUtils>;
}
interface CreateModel {
<... E extends ReduxModelEffects>(model: {
...
effects?: E;
}): { ... }
}
复制代码
而后,须要在逻辑实现上区分 async & generator:
{
...
const mayBePromise = yield effect(effects, action);
mayBePromise['then']
}
复制代码
最终一个全局的 Redux Model 的真实示例:
import createModel from '@tkit/model';
export interface State {
groups: Group[];
scopes: Scope[];
}
const model = createModel({
state: groupModelState,
namespace: 'groupModel',
reducers: {
setGroups: (state, action: Tction<Group[]>): typeof state => {
return {
...state,
groups: action.payload
}
}
},
effects: {
async clearGroupByPromise({ asyncPut }, action: Tction<Group>) {
await asyncPut(model.actions.setGroups, []);
},
*clearGroupByGenerator({ tPut }, action: Tction<Group>): Iterator<{}, any, any> {
yield tPut(model.actions.setGroups, []);
},
}
})
复制代码
类型扩充:
interface CMReducers<S> {
[doSomethingCM: string]: <P extends AbstractAction>(state: S, action: P) => void | S;
}
复制代码
逻辑处理,因为代码逻辑上是区分不了基于 immer 的 reducer 和普通的 reducer,因此必须建立一个新的工厂函数 CM 来作这个事情——因为 reducer 类型的不兼容,因此须要作一个类型转换假装:
interface CM {
<S, R extends CMReducers<S>, E extends ReduxModelEffects>(model: {
state: S;
reducers: R;
effects: E;
}): CreateModel<S, {
[doSomething in keyof R]: (
state: S,
action: Parameters<R[doSomething]>[1]
) => M;
}, E>
}
复制代码
而后就能够欢快的:
import { CM } from '@tkit/model';
const model = CM({
reducers: {
setGroups: (state, action: Tction<Group[]>) => {
state.groups = action.payload;
}
},
...
})
复制代码
不能否认,即使充分的结构化和类型化,不少时候,仍是会感觉到来自 Redux 自己的痛苦——好比,不少状态,并不适合放在全局 Redux ,可是其复杂程度又不能经过 局部的 setState
来管理——
19年2月发布稳定版的 React Hooks useReducer
彷佛可以解决这个问题——依就是个局部 Redux 的模板:
const [state, dispatch] = useReducer((state, action) => {
switch(action.type) { ... }
})
复制代码
有 reducer 有 dispatch,咱们很容易把 Redux Model 复用到 Hooks 上来
hooks effect 只能 async & await,一样也须要一个单独的工厂 M 函数来建立 Hooks Model:
interface M {
<M, R extends Reducers<M>, E extends HooksModelEffects>model: {
state: M;
reducers: R;
effects: E;
}): CreateModel(M, R, E)
}
复制代码
支持 immer 的版本 MM——一样须要类型假装:
interface MM {
<M, R extends CMReducers<M>, E extends HooksModelEffects>(model: {
state: M;
reducers: R;
effects: E;
}): MM<
M,
{
[doSomething in keyof R]: (
state: M,
action: Parameters<R[doSomething]>[1]
) => M;
},
E
>
}
复制代码
useModel 的接口,接收一个 hooks model 和初始状态做为参数:
interface useModel {
<M extends { reducers: any; ... }>(model: M, initialState: M['state'] = model['state']): [
M,
M['actions']
]
}
复制代码
实现 bindDispatchToAction
,将 dispatch 和 model actions 绑定:
interface bindDispatchToAction {
<A, E, M extends { actions: A; effects: E; TYPES: any }>(
actions: A,
dispatch: ReturnType<typeof useReducer>[1],
model: M
): A
}
复制代码
import { MM, useModel } from '@tkit/model';
const UserMMModel = MM({
namespace: 'test',
state: UserMMModelState,
reducers: {
doRename: (state, action: Tction<{ username: string }>) => {
state.username = action.payload.username;
}
},
effects: {
doFetchName: async ({ tPut }, action: Tction<{ time: number }>): Promise<{}> => {
return tPut(UserMMModel.actions.doRename, { username: `${action.payload.time}` });
}
}
});
function Demo() {
const [state, actions] = useModel(UserMMModel);
return (
<>
<h5>{state.username}</h5>
<button onClick={() => actions.doRename('ok')}>1</button>
<button onClick={() => actions.doFetchName(1)}>1</button>
</>
);
}
复制代码