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

如何判断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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.26 17:12:56