0、为什么不使用react-redux
学习react最早使用react-redux,但是写法太麻烦,后来接触蚂蚁金服umi dva这一套体系,开始dva,简化了redux的写法,同时有了model的概念。 后来学习hooks,感觉hooks那种纯函数的写法更加优雅,一直使用hooks一直爽,后面学习到hooks的useContext、useReducer相关api,这些api配合context完全可以代替react-redux。
还有一个原因,有次在b站看教程React Hooks教学教程 | 案例详解 - useReducer,里面看到了这段话,感觉也很有道理:
于是有了以下代码:
1、雏形 useFeature
我有一段时间使用以下这种写法,虽然脱离了react-redux,但是缺点是不支持异步:
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去修改state
使用
import { ContextProvider, useFeature } from './features/index';
const IndexWrap = (props) => (
<ContextProvider>
<App {...props} />
</ContextProvider>
);
export default IndexWrap;
组件中:
缺点
不支持异步,只能请求之后再去dispatch修改state
2、目前的版本:基于context、useContext、useReducer的手写dva--useModel
看过其他人写的帖子分析redux-thunk,好像就是判断action是function还是对象,如果是function就把dispatch传过去,于是仿造这种写法,在原基础上支持异步,写到最后竟然发现其实这不就是dva么。。。
相关目录
index.js:入口文件
core.js: 核心方法
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的调用:
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的异步调用:)
3、实现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的写法
在thunkDispatch中,如果调用的是action,就在调用前setLoading为true,finally的时候就setLoading为false,注意,在loadingModel的reducer中的setLoading方法,如果set为false的时候,其实是删除掉这个属性,并不是真的把loading的值变成false,因为一个应用的请求一般很多,防止loadingModel这个state属性过于多
使用方法
在页面中通过getLoading的方法获取:
总结
基本原理就是使用useContext创建全局状态,useReducer创建dispatch去更新state,主要对dispatch做了封装
组件中哪里需要就引入useModel,随时可以拿到dispatch和state
通过以上的写法,现在项目中基本不需要react-redux了
另外也有局部useModel的写法,详细可以看github :)
如果发现有不对的地方或者建议 欢迎讨论
github项目地址:github.com/jiqishoubi/…
常见问题FAQ
- 免费下载或者VIP会员专享资源能否直接商用?
- 本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
- 提示下载完但解压或打开不了?
- 找不到素材资源介绍文章里的示例图片?
- 模板不会安装或需要功能定制以及二次开发?
发表评论
还没有评论,快来抢沙发吧!