⌛️ 结合 Context API 和 useReducer Hooks封装Model层实践总结

背景:React 的单向数据流模式导致状态只能以 props 的形式从父组件一级一级的传递到子组件,在大中型应用中如果涉及深层嵌套、或者说任意两个组件之间这样跨度较大的通信,我们一般是直接通过全局事件总线(Event Bus)或者引入 Redux 来解决。

React 深层嵌套的组件间通信方式

场景:组件A和组件C都需要展示400手机虚拟号信息,组件B中有一个按钮,点击后会重新调接口获取手机号信息,同时需要更新组件A和组件C的展示。

  1. 全局事件总线
    我们可以通过对 event 的订阅和发布来进行通信。
  • 全局安装 events 第三方库 npm i events --save-dev
  • 创建事件总线并导出:
import { EventEmitter } from 'events';
export const eventBus = new EventEmitter();
  • 监听:组件A和组件C中监听事件
const [phoneNum, setPhoneNum] = useState();
useEffect(()=>{
  eventBus.addListener('getPhone', phoneNum => setPhoneNum(phoneNum));
  return () => {
    eventBus.removeListener('getPhone', () => {})
  }
}, [])
  • 派发:组件B中点击按钮后派发事件
const handleClick = function(){
  eventBus.emit('getPhone', Math.random());
}
<button onClick={() => handleClick()}>更新</button>
  1. Redux
    Redux 来源于 Flux 并借鉴来 Elm 的思想。2015 年,Redux 出现,将 Flux 与函数式编程结合一起,很短时间内就成为了最热门的前端架构。
Redux数据流图.png

View 中事件通过 actionGreator 函数调用 dispatch 发布 action 到 reducers,然后各自的 reducer 根据 action 类型(action.type)来按需更新整个应用的 state。

  • State:表示Model的状态数据
  • Action:改变State的唯一途径。无论是从UI事件、网络回调、还是WebSocket等数据源所获得的数据,最终都会通过 dispatch 函数调用一个 action,从而改变对应的数据。(action必须带有type属性指明具体行为)
  • dispatch函数:用于触发 action 的函数,action是改变State的唯一途径,但它只描述了一个行为,而dispatch可以看作是触发这个行为的方式,而Reducer则描述如何改变数据。
  • Reducer:接受两个参数:之前已经累积运算的结果和当前要被累积的值,返回的是一个新的累积结果。该函数把一个集合归并成一个单值。通过actions中传入的值,与当前reducers中的值进行运算获得新的值(也就是新的state)

👍 优点:

  • 这种数据流的控制可以让应用更可控,以及让逻辑更清晰。

🔧 缺点:

  • 概念太多,并且reducer,saga,action都是分离的(分文件)
  • 编辑成本高,需要在 reducer、saga、action之间来回切换
  • 不便于组织业务模型,比如我们写了一个userlist之后,要写一个productlist,需要复制很多文件。
  • saga 书写太复杂,每监听一个 action 都需要走 fork -> watcher -> worker 的流程
  1. Context API
    随着 React16.3 版本的发布,在深层嵌套这个场景下,有了一个新的答案:使用 Context API
    ⚠️ 注意:React很早就支持context,只是官方不建议使用,因为是一个实验性的API,可能会被改变。但从React 16.3 开始,Context API得到了升级,不再作为不稳定的实验性能力存在,因此可以放心使用。
    🤔️ Q:Context API是干嘛的?
    😯 A:Context 提供了一个无需为每层组件手动添加 props,就能在组件树间进行数据传递的方法。主要用来解决跨组件传参泛滥的问题(prop drilling)。
    当我们想要跨 N 个层级传递某个数据时,逐层传递props就会变得非常繁琐,而且还会带来不必要的数据更新(比如说一些全局性质的数据,用户名、用户权限等)。
    Context 面向这类场景,提供了一种在组件之间共享此类值的方式,它允许我们不必显式地通过组件树的逐层传递 props。
    强力推荐:React新Context API在前端状态管理的实践

Model层封装

  1. 入口文件处理。由于新的 context api 传递过程中不会被 shouldComponentUpdate 阻断,只需要在 Provider 里监听 store 的变化。
import { escContext, action, useMyReducer } from '../models/store.ts';
// ...
const [state, dispatch] = useMyReducer();
return(
  <escContext.Provider value={{state, dispatch}}>
    <A/>
    <B/>
    <C/>
  </escContext.Provider>
)
  1. Model 层 store.ts 封装,这里通过自定义 Hooks useMyReducer 实现异步action的处理。
import { useReducer } from 'react';

export const initialState:TEscState = {
    phone: ''
}
export const escContext = React.createContext<TMixStateAndDispatch>({state: initialState});

export const types = {
    SET_PHONE: 'SET_PHONE',
    GET_PHONE: 'GET_PHONE',
}
export const action = {
    setPhone: (phone: number|string) => {
        return {
            type: types.SET_PHONE,
            phone: phone
        }
    },
    getPhone: (directShowFlag?: boolean|string) => {
        return (dispatch: React.Dispatch<any>, state) => {
            getJsonp('http://mock.test.url....', res => {
                if (res.status == 1) {
                    dispatch(action.setPhone(res.call_num));
                }
            });
        }
    }
}

export const reducer = (state:TEscState, action:TAction) => {
    switch(action.type){
        case types.SET_PHONE:
            return {...state, phone: action.phone}
        default:
            throw new Error('Unexpected action');
    }
}

// 自定义Hooks用于处理异步action
export const useMyReducer = function():[TEscState, React.Dispatch<any>]{
    const [state, dispatch] = useReducer(reducer, initialState);

    function myDispatch(action){
        if(typeof action === 'function'){
            return action(dispatch, state);
        }else{
            dispatch(action);
        }
    }
    return [state, myDispatch]
}
  1. 子组件A、B、C处理
import { escContext, action } from '../../models/store.ts';
const {state, dispatch} = useContext(escContext);

<div>{state.phone}</div>  // state.phone 直接获取数据用于展示
<button onClick={()=>dispatch(action.getPhone())}></button>

小结

本次结合 Context API 和 useReducer Hooks 封装 Model层,统一数据源处理,业务和展示分离,将业务逻辑沉淀在Model层中,便于后期维护。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,835评论 4 364
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,598评论 1 295
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 109,569评论 0 244
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,159评论 0 213
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,533评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,710评论 1 222
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,923评论 2 313
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,674评论 0 203
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,421评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,622评论 2 245
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,115评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,428评论 2 254
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,114评论 3 238
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,097评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,875评论 0 197
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,753评论 2 276
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,649评论 2 271