React中多状态多action类型的reducer与context如何优化实现
多状态多Action场景下的Context与Reducer拆分方案
1. 按职责拆分模块,解决单文件膨胀问题
首先可以将profile相关的逻辑按功能拆分到独立文件,统一放在context/profile/目录下,每个文件职责单一,方便后续维护:
constants.js:存储Action类型常量、初始状态、枚举值,避免硬编码字符串导致的拼写错误reducer.js:拆分的子reducer和组合后的总reducer逻辑services.js:所有异步请求、副作用逻辑context.js:Context实例创建、Provider实现、自定义hook封装index.js:统一导出需要对外暴露的Provider和hook
1.1 抽离常量
constants.js示例代码:
// Action类型常量 export const ACTION_TYPES = { LOADING_START: 'LOADING_START', LOADING_FAILED: 'LOADING_FAILED', LOADING_SUCCESS: 'LOADING_SUCCESS', PROFILES_GET: 'PROFILES_GET', FORM_TYPE_UPDATE: 'FORM_TYPE_UPDATE', PROFILE_EDIT: 'PROFILE_EDIT' } // 初始状态 export const initialState = { status: null, error: null, profiles: [], profile: null, formType: 'Create' }
1.2 拆分Reducer逻辑
把原本单个switch中的case按领域拆分到不同的子reducer,再组合成总reducer,避免单个reducer代码过长:reducer.js示例代码:
import { ACTION_TYPES } from './constants' // 加载状态子reducer function loadingReducer(state, action) { switch(action.type) { case ACTION_TYPES.LOADING_START: return { ...state, status: 'pending' } case ACTION_TYPES.LOADING_SUCCESS: return { ...state, status: 'resolved', error: null } case ACTION_TYPES.LOADING_FAILED: return { ...state, status: 'rejected', error: action.payload } default: return state } } // profile数据子reducer function profileDataReducer(state, action) { switch(action.type) { case ACTION_TYPES.PROFILES_GET: return { ...state, profiles: action.payload } case ACTION_TYPES.PROFILE_EDIT: return { ...state, profile: action.payload } default: return state } } // 表单状态子reducer function formReducer(state, action) { switch(action.type) { case ACTION_TYPES.FORM_TYPE_UPDATE: return { ...state, formType: action.payload } default: return state } } // 组合子reducer得到总reducer export function profileReducer(state, action) { const newState = formReducer(profileDataReducer(loadingReducer(state, action), action), action) if (newState === state) { throw new Error(`Unhandled action type: ${action.type}`) } return newState }
2. 封装自定义Hook,隐藏底层实现
不要直接把dispatch暴露给业务组件,而是在自定义useProfile hook中封装好所有操作方法,减少组件和Context的耦合,同时不需要组件手动传递dispatch:context.js示例代码:
import { createContext, useContext, useReducer } from 'react' import { initialState, profileReducer } from './reducer' import * as profileService from './services' const ProfileContext = createContext(undefined) export function ProfileProvider({ children }) { const [state, dispatch] = useReducer(profileReducer, initialState) return <ProfileContext.Provider value={[state, dispatch]}>{children}</ProfileContext.Provider> } export function useProfile() { const context = useContext(ProfileContext) if (context === undefined) { throw new Error('useProfile must be used within a ProfileProvider') } const [state, dispatch] = context // 封装所有操作方法,直接返回给组件调用 const actions = { getProfiles: () => profileService.getProfiles(dispatch), addProfile: (profile) => profileService.addProfile(dispatch, profile), updateProfile: (profile) => profileService.updateProfile(dispatch, profile), deleteProfile: (profile) => profileService.deleteProfile(dispatch, profile), updateFormType: (type) => dispatch({ type: ACTION_TYPES.FORM_TYPE_UPDATE, payload: type }), setEditProfile: (profile) => dispatch({ type: ACTION_TYPES.PROFILE_EDIT, payload: profile }) } return [state, actions] }
优化后表单组件的代码会更简洁:
function Form() { const { register, handleSubmit } = useForm(); const [{ formType, profile }, { addProfile, updateProfile }] = useProfile(); const buttonText = formType === 'Create' ? 'Create' : 'Edit'; React.useEffect(() => { if(formType === 'Edit'){ // 填充表单值 } }, [formType]) const onSubmit = data => { const request = formType === 'Create' ? addProfile : updateProfile request(data).catch(error => toast(error)) } return ( <form onSubmit={handleSubmit(onSubmit)}> <input {...register("name", { required: true })} /> <input {...register("age", { required: true })} /> <button>{buttonText}</button> </form> ) }
3. 可扩展性优化建议
- 后续新增功能时,只需要在对应模块添加逻辑即可:新增接口就往
services.js加方法,新增状态变更就加对应Action和子reducer的case,需要对外暴露的方法在useProfile的actions中添加即可。 - 如果后续状态规模进一步扩大,可以按领域拆分Context:比如把profile列表数据相关的状态和表单操作相关的状态拆分为两个独立的Context,各自维护自己的reducer和状态,业务组件只需要消费自己需要的Context,避免无关状态更新导致的不必要重渲染。
- 不需要过早拆分Context,当前规模下按模块拆分逻辑已经足够应对,等后续出现明显的重渲染性能问题或者状态职责完全独立时再拆分即可。
内容的提问来源于stack exchange,提问作者camille
相关产品推荐
相关产品推荐

