使用useReducer管理组件状态的思维模型

217 阅读5分钟

前言:这我的前一篇文章Say Bey to useState中我分享了尽可能地使用useReducer来代替useState的观点,但是在实际开发中发现如何实操还是有很多思考点和抉择点,所以这篇文章中我想继续阐述下使用useReducer管理组件状态时的思维模型

关键元素

该模型下有三个关键的元素:state、action和effect

我们要做的就是利用这三块积木组装我们整个业务逻辑。

提取关键状态

首先关注state,拿到一个组件的开发需求我们首要的任务就是从茫茫多的可能状态组合中提取中关键状态。

{
    stateType: EStateTypes.LARGE,
    showCoins: false,
    showPraiseWord: false,
    canClick: true,
    coins: 0,
    showType: EShowTypes.LARGE,
    showIcon: true,
    svgaData: {
        path: `static/svgas/${tpl}/checkin/checkin.svga`,
        // 循环播放次数
        loops: 0,
        // 从第0开始
        startFrame: 0,
        // 22帧前为未签到动画
        endFrame: 22,
    }
}

这是我最近开发的宝箱组件的初始状态,这都是和UI强相关的,但仍然有这么一坨,这些个字段的可能值可以组合出N多的可能状态,没人能够处理这些状态,然而实际上这些组合里仅仅有少数的几个是可以被用户操作出来或者说是需要关注的。

我们要做的就是找到这几个需关注状态

就以这个宝箱组件为例,有哪些状态是需要关注的呢?

1.初始时居中大宝箱状态

2.左下角小宝箱状态

3.大宝箱时被打开

4.小宝箱时被打开

5.展示开宝箱文案

6.关闭

通过action修改state

通过Action使组件从初始状态到达这些状态,这部分逻辑就是reducer了。

定义reducer就像这样:

const reducer = (state: IState, action: Action) => {
    switch (action.actionType) {
        case EActionTypes.OPEN:
            return {
                ...state,
                stateType: state.showType === EShowTypes.LARGE
                    ? EStateTypes.OPENED_WHEN_LARGE
                    : EStateTypes.OPENED_WHEN_SMALL,
                showCoins: state.showType === EShowTypes.LARGE,
                coins: action.coins,
                svgaData: {
                    ...state.svgaData,
                    loops: 1,
                    startFrame: 23,
                    endFrame: Infinity
                },
                canClick: false
            };
        // ...
    }
};

使用动作更改状态时就像这样:

// 点击开宝箱按钮触发的函数
function checkinRequest() {
        service.post<IReqParams, IResCheckinData>(Api.checkIn, getParams()).then(({code, data}) => {
            if (code === 0) {
                const {coins, message} = data;
                // 如果后端查询到已经开过宝箱则提示用户后关闭
                if (coins === -1) {
                    Message.toast(message);
                    dispatch({actionType: EActionTypes.CLOSE});
                }
                // 正常进行打开动作
                dispatch({actionType: EActionTypes.OPEN, coins});
                // 打开后还要进行请求接口获取本次打开对应的表扬文案等操作,你会在这里书写这个逻辑吗?
            }
        });
    }

在正确的位置书写effect

注意,重点来了。 我们打开动作后还要执行一系列操作,如请求接口获取这次打开对应的文案,执行一些动画,通知其他组件等。

这些操作其实都是effect,不要直接在dispatch后的代码块里书写effect的逻辑,而是应该放到useEffect中,当stateType改变后再执行对应的effect

// 当组件状态改变时执行对应的effect
useEffect(() => {
    switch (stateType) {
        case EStateTypes.OPENED_WHEN_SMALL:
            getPraiseData(Api.getPraiseWord, getParams()).then(praiseWordList => {
                // 通知其他组件更新UI
                notifyCommentAreaUpdate(praiseWordList.join(', '));
                // 关闭动作
                dispatch({actionType: EActionTypes.CLOSE});
            });
            break;
        // 点击大宝箱到达此状态
        case EStateTypes.OPENED_WHEN_LARGE:
            getPraiseData(Api.getPraiseWord, getParams()).then(praiseWordList => {
                // 变
                dispatch({
                    actionType: EActionTypes.SHOW_PRAISE,
                    praiseWordList
                });
                notifyCommentAreaUpdate(praiseWordList.join(', '));
            });
            // 飞金币
            flyCoins({coinRef, coinIcon}).then(() => {
                dispatch({actionType: EActionTypes.CLOSE});
            });
            break;
       // ...

    }
}, [stateType]);

这里就是核心代码,这里要注意避免写出这种我觉得不太好的代码,就是case为XXX时直接一个函数调用,所有的发ajax请求以及请求后的dispatch都在这个函数里。

因为我并不关心异步操作的具体逻辑,只关心进行了什么异步操作,操作后要执行什么action,而如果像那样只有一个函数调用,我就得离开这个代码块,搜索这个函数,阅读一些不重要的具体异步操作逻辑,然后才能看到我想知道的执行了什么action, 这样阅读体验就大大降低。

而(我个人觉得)正确的做法是,可以看情况封装下异步方法,比如:

// 把onLastEnd这种风格的异步函数封装成Promise
export const flyCoins = (flyDeps: IFlyDeps) => new Promise<void>((resolve, _) => {
    const {coinRef, coinIcon} = flyDeps;
    new FlyImg({
        startEl: coinRef.current,
        imgOption: {
            src: coinIcon,
            width: 30,
            height: 30,
        },
        onLastEnd: resolve
    });
});

然后在使用时就

// 飞金币
flyCoins({coinRef, coinIcon}).then(() => {
    dispatch({actionType: EActionTypes.CLOSE});
});

这样我就是在stateType为OPENED时,执行飞金币的异步逻辑,具体怎么实现的我不管,反正飞完了我要执行CLOSE动作。这样就把不重要的逻辑剔除了stateType为OPENED的case。我只需要阅读这个useEffect(effect, [stateType]);就可以理清绝大部分的组件逻辑了

为什么说是绝大部分而不是全部?

其实剩下的大致有两个可能:

  • 其他的useEffect,比如监听其他全局状态改变执行一些effect
  • UI Event Handler,比如响应点击开宝箱事件执行的逻辑

What is effect?

最后还想提个有趣的小问题,React Hooks语境下的effect的到底是什么呢?

我目前觉得effect = async action + side effect;

所谓的async action说白了就是先等我执行完一段异步逻辑之后再做动作。从上面的例子来看

flyCoins({coinRef, coinIcon}).then(() => {
    dispatch({actionType: EActionTypes.CLOSE});
});

这段代码就是一个async action,当宝箱被打开之后,我们要执行这个异步动作,也就是飞金币动画后执行关闭动作。

side effect就是值影响其他组件的行为,比如改变globalStore里的数据,调用SDK方法通知中台,I/O操作等。

以上就是我目前阶段开发组件的思维模型,如果你觉得有问题或者有better practice欢迎在评论中指出。Thanks for reading~