@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>
</>
);
}
複製代碼