基於hooks api手寫dva----useModel

0、爲何不使用react-redux

學習react最先使用react-redux,可是寫法太麻煩,後來接觸螞蟻金服umi dva這一套體系,開始dva,簡化了redux的寫法,同時有了model的概念。 後來學習hooks,感受hooks那種純函數的寫法更加優雅,一直使用hooks一直爽,後面學習到hooks的useContext、useReducer相關api,這些api配合context徹底能夠代替react-redux。react

還有一個緣由,有次在b站看教程React Hooks教學教程 | 案例詳解 - useReducer,裏面看到了這段話,感受也頗有道理:webpack

WechatIMG1059.png

因而有了如下代碼:git

一、雛形 useFeature

我有一段時間使用如下這種寫法,雖然脫離了react-redux,可是缺點是不支持異步:github

import React, { useReducer, useContext } from 'react';

/** * context */
const IndexContext = React.createContext();

/** * reducer */
const initialState = {
  lookingRecord: null,
};

const initStateFunc = () => {
  return initialState;
};

const reducer = (state, action) => {
  const { type, ...restState } = action;

  if (type == 'SAVE') {
    return {
      ...state,
      ...restState,
    };
  } 
  
  return state;
};

const ContextProvider = (props) => {
  const [state, dispatch] = useReducer(reducer, initialState, initStateFunc);

  return (
    <IndexContext.Provider value={{ state, dispatch }}>{props.children}</IndexContext.Provider>
  );
};

/** * hooks */
const useFeature = () => {
  const { state, dispatch } = useContext(IndexContext);

  return {
    state,
    dispatch,
  };
};

export { ContextProvider, useFeature };

複製代碼

最終暴露出來的的是一個provider和一個userFeatrue,其實就是React.createContext建立了一個context,去包裹我想共享數據的頁面或者組件,而後被包裹的組件裏去使用useFeature就能讀取到state也能拿到dispatch去修改stateweb

使用

import { ContextProvider, useFeature } from './features/index';

const IndexWrap = (props) => (
  <ContextProvider> <App {...props} /> </ContextProvider>
);

export default IndexWrap;
複製代碼

組件中:redux

WechatIMG1055.png

缺點

不支持異步,只能請求以後再去dispatch修改stateapi

二、目前的版本:基於context、useContext、useReducer的手寫dva--useModel

看過其餘人寫的帖子分析redux-thunk,好像就是判斷action是function仍是對象,若是是function就把dispatch傳過去,因而仿造這種寫法,在原基礎上支持異步,寫到最後居然發現其實這不就是dva麼。。。promise

相關目錄

dva目錄.png

index.js:入口文件markdown

core.js: 核心方法antd

global.js和login.js是不一樣的模塊,根據業務能夠在index.js中添加

咱們先看入口文件:

index.js

import React from "react";
import { generateLoadingModel, generateProvider, generateUseModel } from "@/models/core.js";
import global from "./global";
import login from "./login";

const allModel = {
    loading: generateLoadingModel(),
    global,
    login
};

/** * redux */
const IndexContext = React.createContext();

// Provider
const ContextProvider = generateProvider({
    context: IndexContext,
    allModel
});

/** * @returns {{ * state:object; * dispatch:function; * getLoading:function; * }} */
const useModel = generateUseModel({
    context: IndexContext,
    allModel
});

export { ContextProvider, useModel };

複製代碼

最終暴露出去的就是一個provider和一個useModel,這裏provider和useModel的使用方式和原來的useFeature是同樣的,provider最外層包裹,內部使用useModel能夠拿到state和dispatch

看幾個關鍵的方法:

generateUseModel : 生成一個useModel,關鍵代碼:裏面的thunkDispatch方法

/** * 生成一個useModel * @param {object} options * @param {*} options.context * @param {object} options.allModel * @param {function} [options.dealExport] */
export function generateUseModel({ context, allModel, dealExport }) {
    /** * @returns {{ * state, * dispatch, * getLoading * }} */
    return function () {
        const { state, dispatch } = useContext(context);

        const stateRef = useRef(state);

        useEffect(() => {
            stateRef.current = state;
        }, [state]);

        function getState() {
            return { ...stateRef.current };
        }

        // loading
        function setLoading(type, flag) {
            dispatch({
                modelName: "loading",
                methodName: "setLoading",
                payload: {
                        type,
                        loading: flag
                },
                dispatch: thunkDispatch
            });
        }

        function getLoading(type) {
            return state.loading[type] || false;
        }

        /** * 最終暴露出去的,外面用到的dispatch 能夠處理異步 * @param {string} type // 'global/toggle' * @param {*} payload // 自定義攜帶參數 */
        function thunkDispatch(type, payload) {
            const modelName = type.split("/")[0];
            const methodName = type.split("/")[1];

            const modelAction = allModel[modelName].actions?.[methodName];

            if (modelAction) {
                // 異步
                setLoading(type, true);
                modelAction({
                    getState,
                    payload,
                    dispatch: thunkDispatch
                }).finally(() => {
                        setLoading(type, false);
                });
            } else {
                // 同步
                dispatch({
                    modelName,
                    methodName,
                    payload,
                    dispatch: thunkDispatch
                });
            }
        }

        // 暴露出去
        const defaultExport = {
            state,
            dispatch: thunkDispatch,
            getLoading
        };

        if (dealExport) {
            return dealExport(defaultExport);
        }
        return defaultExport;
    };
}
複製代碼

這裏和雛形版useFeature的區別最大的一點是 咱們暴露出去的dispatch並非useReducer()出來的原生的dispatch,是thunkDispatch,這裏先看一下model的結構和dispatch(指thunkDispatch)在組件中的使用方法:

model的結構:

例:globalModel

function toggleCollapsedAjax(flag) {
    return new Promise((resolve) => {
        setTimeout(() => {
            resolve(!flag);
        }, 1000);
    });
}

const initialState = {
    isCollapsed: false
};

const model = {
    name: "global",
    state: initialState,
    actions: {
        async toggleCollapsedFunc({ dispatch, getState, payload }) {
            await toggleCollapsedAjax();
            const state = getState().global;
            dispatch("global/save", {
                isCollapsed: !state.isCollapsed
            });
        }
    },
    reducers: {
        save({ state, payload }) {
            return {
                ...state,
                ...payload
            };
        }
    }
};

export default model;
複製代碼

這裏actions是異步的方法,reducers是同步的方法

看下組件中dispatch的調用:

WechatIMG1056.png

thunkDispatch方法接收兩個參數,type是這種格式:'global/toggleCollapsedFunc'

"/"以前是模塊名字,後面是調用的方法,能夠是action也能夠是reducer,咱們在thunkDispatch方法中會去判斷, 若是是action就先調用action同時把dispatch傳進去,若是不是action那就走原生的dispatch,它會觸發原生的reducer:

/** * 生成reducer * @param {object} options * @param {object} options.allModel * @returns */
export function generateReducer({ allModel }) {
    /** * reducer 原生的dispatch觸發的 就是這個 * @param {*} allState * @param {object} options * @returns */
    return function (allState, options) {
        const { modelName, methodName, payload, dispatch } = options;

        const modelReducer = allModel[modelName].reducers?.[methodName];

        const oldModelState = allState[modelName];
        // 調用model裏的reducer
        const newModelState = modelReducer({
            state: oldModelState,
            payload,
            dispatch
        });

        return lodash.cloneDeep({
            ...allState,
            [modelName]: newModelState
        });
    };
}
複製代碼

調用對應model裏的reducer方法去生成一個新的state,而後根據modelName去修改整個state

這樣就是實現了redux的異步調用:)

三、實現dva的loading

dva框架有個方便的地方,就是當你調用一個異步請求的時候,它會自動生成一個loading的狀態,看一下我是如何實現的:

allModel中總會有一個loadingModel,它是經過generateLoadingModel這個方法生成的:

/** * 生成一個loading model * @returns model */
export function generateLoadingModel() {
    return {
        name: "loading",
        state: {},
        reducers: {
            setLoading({ state, payload, dispatch }) {
                const { type, loading } = payload;

                const newState = { ...state };

                if (loading) {
                        newState[type] = true;
                } else {
                        delete newState[type];
                }

                return newState;
            }
        }
    };
}
複製代碼

由於每一個model中的action都是promise,能夠使用.finally的寫法

WechatIMG1070.png

在thunkDispatch中,若是調用的是action,就在調用前setLoading爲true,finally的時候就setLoading爲false,注意,在loadingModel的reducer中的setLoading方法,若是set爲false的時候,實際上是刪除掉這個屬性,並非真的把loading的值變成false,由於一個應用的請求通常不少,防止loadingModel這個state屬性過於多

看一下setLoading方法:

// loading
    function setLoading(type, flag) {
        dispatch({
            modelName: "loading",
            methodName: "setLoading",
            payload: {
                type,
                loading: flag
            },
            dispatch: thunkDispatch
        });
    }

    function getLoading(type) {
        return state.loading[type] || false;
    }
複製代碼

使用方法

在頁面中經過getLoading的方法獲取:

WechatIMG1057.png

總結

基本原理就是使用useContext建立全局狀態,useReducer建立dispatch去更新state,主要對dispatch作了封裝

組件中哪裏須要就引入useModel,隨時能夠拿到dispatch和state

經過以上的寫法,如今項目中基本不須要react-redux了

另外也有局部useModel的寫法,詳細能夠看github :)

若是發現有不對的地方或者建議 歡迎討論

github項目地址:github.com/jiqishoubi/…

相關文章
相關標籤/搜索