You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何遵循关注点分离重构React reducer 消除loading状态代码重复

问题解答

是不是代码重复?

确实属于典型的跨模块通用逻辑重复。所有需要处理异步请求的业务模块,都重复实现了3种Loading状态的reducer分支、以及异步请求内的try/catch状态分发模板代码,后续如果要调整loading相关逻辑,所有模块都要同步修改,维护成本很高。

如何解决重复问题?

方案1:基于现有reducer架构改造(最小改造成本)

无需推翻现有代码,通过高阶reducer和通用异步请求包装函数就能抽离通用loading逻辑,所有业务模块都可以复用:

  1. 抽离高阶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)
    }
  }
}
  1. 改造原有业务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
})
  1. 抽离通用异步请求包装函数,去掉重复的模板代码
// 接收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
  }
}
  1. 改造原有接口请求逻辑
// 简化后的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逻辑,根据你的场景分两种用法:

  1. 如果状态不需要跨多层组件共享,直接在业务组件内使用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>
}
  1. 如果状态需要全局共享,可以把useAsync的返回值放到Context中,代替原有reducer方案,同样可以实现loading逻辑的全局复用。

内容的提问来源于stack exchange,提问作者camille

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.10.01 09:06:00