如何遵循关注点分离重构React reducer 消除loading状态代码重复
问题解答
是不是代码重复?
确实属于典型的跨模块通用逻辑重复。所有需要处理异步请求的业务模块,都重复实现了3种Loading状态的reducer分支、以及异步请求内的try/catch状态分发模板代码,后续如果要调整loading相关逻辑,所有模块都要同步修改,维护成本很高。
如何解决重复问题?
方案1:基于现有reducer架构改造(最小改造成本)
无需推翻现有代码,通过高阶reducer和通用异步请求包装函数就能抽离通用loading逻辑,所有业务模块都可以复用:
- 抽离高阶reducer处理通用loading状态
// 通用高阶reducer,接收业务reducer和业务初始状态作为参数 function withAsyncReducer(businessReducer, businessInitialState) { return function combinedReducer(state = { status: null, error: null, ...businessInitialState }, action) { switch(action.type) { case 'LOADING_START': return {...state, status: 'pending', error: null} case 'LOADING_FAILED': return {...state, status: 'rejected', error: action.payload} case 'LOADING_SUCCESS': return {...state, status: 'resolved', error: null} // 非loading相关的action交给业务reducer自行处理 default: return businessReducer(state, action) } } }
- 改造原有业务reducer,删掉重复的loading分支
// 仅保留profile业务相关的处理逻辑 const baseProfileReducer = (state, action) => { switch (action.type) { case 'PROFILES_GET': return { ...state, profiles: action.payload } case 'PROFILE_EDIT': return { ...state, profile: action.payload } default: throw new Error(`Unhandled action type: ${action.type}`) } } // 用高阶reducer包装得到最终的profileReducer const profileReducer = withAsyncReducer(baseProfileReducer, { profiles: [], profile: null })
- 抽离通用异步请求包装函数,去掉重复的模板代码
// 接收dispatch、请求函数、成功后的action作为参数 const wrapAsyncRequest = async (dispatch, requestFn, successActionHandler) => { dispatch({type: 'LOADING_START'}) try { const result = await requestFn() dispatch({type: 'LOADING_SUCCESS'}) if(successActionHandler) { dispatch(successActionHandler(result)) } return result } catch (error) { dispatch({type: 'LOADING_FAILED', payload: error}) throw error } }
- 改造原有接口请求逻辑
// 简化后的getProfiles async function getProfiles(dispatch) { return wrapAsyncRequest(dispatch, client.getProfiles, (profiles) => ({ type: 'PROFILES_GET', payload: profiles })) } // 简化后的addProfile async function addProfile(dispatch, profile) { return wrapAsyncRequest(dispatch, async () => { await client.addProfile(profile) return client.getProfiles() }, (profiles) => ({ type: 'PROFILES_GET', payload: profiles })) }
后续其他业务模块也可以直接复用withAsyncReducer和wrapAsyncRequest,不需要再写重复的loading相关逻辑。
方案2:使用useAsync实现复用
完全可以用useAsync来复用loading逻辑,根据你的场景分两种用法:
- 如果状态不需要跨多层组件共享,直接在业务组件内使用useAsync即可,不需要再维护reducer里的loading状态:
function ProfileList() { const {data: profiles, status, error, run} = useAsync() useEffect(() => { run(client.getProfiles()) }, [run]) if(status === 'pending') return <Loading组件 /> if(status === 'rejected') return <Error提示组件 error={error} /> return <div>{profiles.map(item => <ProfileItem key={item.id} data={item} />)}</div> }
- 如果状态需要全局共享,可以把useAsync的返回值放到Context中,代替原有reducer方案,同样可以实现loading逻辑的全局复用。
内容的提问来源于stack exchange,提问作者camille
相关产品推荐
相关产品推荐

