TypeScript + React最佳實踐-第二節:@tkit/model - React 全局和局部狀態管理方案

@tkit/model-React全局和局部狀態管理管理方案

備註

@tkit/* 是內部版本,替換成 tkit-* 則是對外版本react

繁複 + 的 Redux

18 年 9 月的時候,咱們團隊開始全面推廣 TypeScript + React + Redux 的方案,其中 Redux 這塊方案選型:redux

  • redux-actions 建立 action 和 reducer
  • redux-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

後來者的優點是總有別人的經驗可以參考,大型實踐以後,咱們引入了 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

  1. reducers 和 effects 結構不對稱
  2. effects 僅支持 generator,不支持 async 函數
  3. 單個 effect 裏對 reducer 的調用是基於字符串,沒法作到類型化
  4. reducers、effects 裏每一個方法的 action 參數的類型不能和 bindActionCreators 貫通

對於不足 1——調整 effect 參數順序便可:編碼

interface Model<S extends any> {
    ...
    effects: {
        [doSomethingAsync: string]: (utils: SagasUtils, action: Action<any>) => Iterator<{}, any, any>;
    };
    ...
}
複製代碼

而其餘不足,則須要另闢蹊徑spa

Model

單個 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>
        }
    }
複製代碼

實現 reducers 和 state 類型貫通

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 內觸發 reducer 的類型化

在 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);
    ...
}
複製代碼

實現 effects 支持 async 函數

首先,類型定義上擴充 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']
}
複製代碼

Demo

最終一個全局的 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, []);
        },
  }
})
複製代碼

【增強】reducer 支持 immer

類型擴充:

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 自己的痛苦——好比,不少狀態,並不適合放在全局 Redux ,可是其複雜程度又不能經過 局部的 setState 來管理——

19年2月發佈穩定版的 React Hooks useReducer 彷佛可以解決這個問題——依就是個局部 Redux 的模板:

const [state, dispatch] = useReducer((state, action) => {
    switch(action.type) { ... }
})
複製代碼

有 reducer 有 dispatch,咱們很容易把 Redux Model 複用到 Hooks 上來

Hooks Model

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

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
}
複製代碼

Demo

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>
        </>
    );
}
複製代碼
相關文章
相關標籤/搜索