如何判断Redux Thunk请求是否Pending以避免重复派发动作?
问题描述
我有一个基于React+Redux Toolkit的应用,通过REST API获取数据并存入Redux。为减少新增接口的样板代码,我封装了基于createAsyncThunk的工具函数、生成extraReducers的方法,还做了带认证的API请求封装。但部分组件(比如OAuth回调组件)在挂载时会派发action,现在的问题是无法判断请求是否已发送,导致重复派发动作。不想在组件侧用useState跟踪状态,想找更优雅的通用设计模式。
相关代码:
// wrapper around data returned from API export interface AsyncState<T> { status: RequestStatus, value: T, error: Error | string } // wrapper around API requests to provide an authenticated axios client export const authRequest = <ActionReturnType, ActionArgumentType=null>(method: (params: ActionArgumentType & {client: AxiosInstance}) => Promise<ActionReturnType>) => async (param: ActionArgumentType, thunkAPI): Promise<ActionReturnType> => { const state = thunkAPI.getState() if (!state.##MY_AUTH_PATH##) { throw new Error(`auth not set`) } const token = `Bearer ${state.##MY_AUTH_PATH##}` const client = axios.create({ headers: { Authorization: token }, }) const data = await method({ ...param, client }) return data } // method run in 'extraReducers' part of slice creation export const makeAsyncReducer = < ActionReturnType, // type returned from async action ActionArgumentType, // type of argument passed to async action SliceState // the type of state of the reducer combiner >( builder: ActionReducerMapBuilder<SliceState>, action: AsyncThunk<ActionReturnType, ActionArgumentType, AsyncThunkConfig>, formatValue: (state: Draft<SliceState>, v: ActionReturnType) => DeepPartial<Draft<SliceState>> ) => { builder.addCase(action.fulfilled, (state, action) => { state = ObjectUtils.mergeDeep( state, formatValue(state, action.payload) ) return state }) builder.addCase(action.pending, (state, action) => { // unsure of how to implement }) builder.addCase(action.rejected, (state, action) => { state = ObjectUtils.mergeDeep( state, _.chain({}) .set(`${action.error.state_key}.status`, RequestStatus.Rejected) .set(`${action.error.state_key}.value`, null) .set(`${action.error.state_key}.error`, action.error.error) .value() ) return state }) } // example action export const getMe = createAppAsyncThunk( 'profile', authRequest(API.getMe) ) const slice = createSlice({ name: 'api', initialState, reducers: {}, extraReducers: builder => { builder.addCase(setChallenge, (state, action) => { const c = current(state) const updated = ObjectUtils.mergeDeep(c, { auth: { value: { challenge: action.payload } } }) return updated }) makeAsyncReducer(builder, exchangeCode, (state, response) => { return ObjectUtils.mergeDeep(state, { auth: { status: RequestStatus.Fulfilled, value: response } }) }) makeAsyncReducer(builder, getMe, (state, response) => { return ObjectUtils.mergeDeep(state, { profile: { value: response } }) }) } })
通用解决方案
方案1:基于Redux状态做请求拦截(推荐)
利用你已经定义的AsyncState接口,完善makeAsyncReducer的pending状态处理,把每个请求的状态存在Redux里,组件派发前先检查状态:
步骤1:完善makeAsyncReducer的pending逻辑
给makeAsyncReducer增加一个stateKey参数,明确请求对应的状态路径,统一处理pending/fulfilled/rejected状态:
export const makeAsyncReducer = < ActionReturnType, ActionArgumentType, SliceState >( builder: ActionReducerMapBuilder<SliceState>, action: AsyncThunk<ActionReturnType, ActionArgumentType, AsyncThunkConfig>, stateKey: string, // 新增:指定该请求对应的AsyncState路径,比如'auth'、'profile' formatValue?: (state: Draft<SliceState>, payload: ActionReturnType) => void ) => { builder.addCase(action.pending, (state) => { const asyncState = _.get(state, stateKey) as Draft<AsyncState<any>>; asyncState.status = RequestStatus.Pending; asyncState.error = null; }); builder.addCase(action.fulfilled, (state, action) => { const asyncState = _.get(state, stateKey) as Draft<AsyncState<ActionReturnType>>; asyncState.status = RequestStatus.Fulfilled; asyncState.error = null; if (formatValue) { formatValue(state, action.payload); } else { asyncState.value = action.payload; } }); builder.addCase(action.rejected, (state, action) => { const asyncState = _.get(state, stateKey) as Draft<AsyncState<any>>; asyncState.status = RequestStatus.Rejected; asyncState.error = action.error.message || '请求失败'; asyncState.value = null; }); }; // 使用示例 makeAsyncReducer(builder, getMe, 'profile'); makeAsyncReducer(builder, exchangeCode, 'auth', (state, payload) => { state.auth.value = { ...state.auth.value, ...payload }; });
步骤2:组件中根据状态派发请求
用useSelector获取请求状态,仅当状态不是Pending/Fulfilled时才派发:
const profileState = useSelector((state) => state.api.profile); const dispatch = useDispatch(); useEffect(() => { if (profileState.status !== RequestStatus.Pending && profileState.status !== RequestStatus.Fulfilled) { dispatch(getMe()); } }, [dispatch, profileState.status]);
进阶:封装成自定义Hook
把状态检查逻辑封装成Hook,减少组件重复代码:
export const useAsyncAction = <T>( action: AsyncThunk<any, any, any>, selector: (state: RootState) => AsyncState<T> ) => { const dispatch = useDispatch(); const state = useSelector(selector); const trigger = useCallback((...args: any[]) => { if (state.status !== RequestStatus.Pending && state.status !== RequestStatus.Fulfilled) { dispatch(action(...args)); } }, [dispatch, state.status, action]); return { ...state, trigger }; }; // 组件中使用 const { trigger: getProfile } = useAsyncAction(getMe, (state) => state.api.profile); useEffect(() => { getProfile(); }, [getProfile]);
方案2:用RTK Listener Middleware全局控制请求时机
把请求触发逻辑从组件移到Redux中间件,比如当认证完成后自动触发getMe,避免组件挂载时盲目派发:
// 创建Listener Middleware const listenerMiddleware = createListenerMiddleware(); // 添加监听器:当auth状态变为已认证时触发getMe listenerMiddleware.startListening({ predicate: (_, currentState, previousState) => { return previousState.api.auth.status !== RequestStatus.Fulfilled && currentState.api.auth.status === RequestStatus.Fulfilled; }, effect: async (_, listenerApi) => { const profileState = listenerApi.getState().api.profile; if (profileState.status !== RequestStatus.Pending && profileState.status !== RequestStatus.Fulfilled) { await listenerApi.dispatch(getMe()); } }, }); // 配置Store时添加中间件 export const store = configureStore({ reducer: { api: apiSlice.reducer }, middleware: (getDefaultMiddleware) => getDefaultMiddleware().prepend(listenerMiddleware.middleware), });
方案3:在Thunk内部拦截重复请求
在authRequest封装中加入内存缓存,同一请求正在进行时直接返回已有Promise,避免重复发送:
// 存储正在进行的请求,键为action名称+参数哈希 const pendingRequests = new Map<string, Promise<any>>(); export const authRequest = <ActionReturnType, ActionArgumentType=null>(method: (params: ActionArgumentType & {client: AxiosInstance}) => Promise<ActionReturnType>) => async (param: ActionArgumentType, thunkAPI): Promise<ActionReturnType> => { const actionName = thunkAPI.type; const paramKey = JSON.stringify(param || {}); const requestKey = `${actionName}:${paramKey}`; // 已有相同请求在进行,直接返回该Promise if (pendingRequests.has(requestKey)) { return pendingRequests.get(requestKey)!; } const state = thunkAPI.getState() as RootState; if (!state.##MY_AUTH_PATH##) { throw new Error(`auth not set`) } const token = `Bearer ${state.##MY_AUTH_PATH##}`; const client = axios.create({ headers: { Authorization: token }, }); try { const requestPromise = method({ ...param, client }); pendingRequests.set(requestKey, requestPromise); return await requestPromise; } finally { // 请求完成后移除缓存 pendingRequests.delete(requestKey); } };
内容的提问来源于stack exchange,提问作者maxwellray
相关产品推荐
相关产品推荐

