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

如何在NgRx中解决多个Angular组件并发请求同一对象时的重复XHR请求问题?

解决Angular NgRx并发请求重复API调用的问题

这个场景是NgRx开发中非常典型的重复请求竞态问题——当多个组件同时触发同一用户的加载动作时,因为第一个请求还没返回、store里还没数据,所有请求都会穿透到API。只在effect里检查现有实体是不够的,我们需要在请求发起阶段就做好「去重」控制,给你几个实用的解决方案:

方案一:在Effect中追踪请求状态,过滤重复请求

核心思路是在store里维护一个「正在加载的用户ID列表」,当新的LoadUser动作进来时,先判断该用户是否已经在加载中,避免重复发起请求。

1. 更新状态定义

首先扩展你的用户状态,加入加载中的ID集合:

interface UserState {
  entities: { [id: number]: User };
  loadingUserIds: number[]; // 记录当前正在请求的用户ID
}

2. 新增辅助动作和Reducer

添加用于更新加载队列的动作:

export const AddLoadingUserId = createAction(
  '[User] Add Loading User ID',
  props<{ id: number }>()
);

export const RemoveLoadingUserId = createAction(
  '[User] Remove Loading User ID',
  props<{ id: number }>()
);

然后在reducer里处理这些动作:

const userReducer = createReducer(
  initialState,
  on(AddLoadingUserId, (state, { id }) => ({
    ...state,
    loadingUserIds: [...state.loadingUserIds, id]
  })),
  on(RemoveLoadingUserId, (state, { id }) => ({
    ...state,
    loadingUserIds: state.loadingUserIds.filter(userId => userId !== id)
  })),
  // 其他现有reducer逻辑...
);

3. 修改Effect逻辑

在effect里加入过滤逻辑,只有当用户不存在且不在加载队列时,才发起API请求:

loadUser$ = createEffect(() => 
  this.actions$.pipe(
    ofType(LoadUser),
    withLatestFrom(this.store$.select(selectUserState)),
    // 双重过滤:用户未加载 + 未在请求中
    filter(([action, userState]) => {
      return !userState.entities[action.id] && !userState.loadingUserIds.includes(action.id);
    }),
    // 先把当前用户ID加入加载队列
    tap(([action]) => this.store.dispatch(AddLoadingUserId({ id: action.id }))),
    switchMap(([action]) => 
      this.userService.getUser(action.id).pipe(
        map(user => LoadUserSuccess({ user })),
        catchError(err => of(LoadUserFailure({ id: action.id, err }))),
        // 请求结束(成功/失败)后,移除加载队列中的ID
        finalize(() => this.store.dispatch(RemoveLoadingUserId({ id: action.id })))
      )
    )
  )
);

这样一来,同时触发的多个LoadUser动作中,只有第一个会通过过滤发起请求,后续的都会被拦截,直到请求结束。

方案二:在Service层实现请求缓存复用

另一种更简洁的方式是在用户服务层做缓存,让同一个用户的请求复用同一个Observable,自然避免重复API调用。

修改UserService

@Injectable({ providedIn: 'root' })
export class UserService {
  // 用Map缓存正在请求或已完成的用户Observable
  private userRequestCache = new Map<number, Observable<User>>();

  constructor(private http: HttpClient) {}

  getUser(id: number): Observable<User> {
    if (!this.userRequestCache.has(id)) {
      const user$ = this.http.get<User>(`/api/users/${id}`).pipe(
        shareReplay(1), // 缓存结果,后续订阅直接复用
        finalize(() => {
          // 可选:请求完成后移除缓存,允许后续重新请求;如果需要持久缓存可以去掉这行
          this.userRequestCache.delete(id);
        })
      );
      this.userRequestCache.set(id, user$);
    }
    return this.userRequestCache.get(id)!;
  }
}

之后你的effect不需要复杂的状态判断,直接调用这个service即可——即使多个effect同时调用getUser,也会订阅同一个Observable,只会发起一次API请求。

方案三:使用NgRx Entity的加载状态管理

如果你的项目已经在使用NgRx Entity,可以利用它的实体管理能力,结合selectIds和自定义的加载状态来实现去重,思路和方案一类似,但更贴合NgRx的最佳实践。比如用entityAdapter.getSelectors()获取已加载的用户ID,再结合加载队列判断是否需要发起请求。

补充:你的组件代码小问题

顺便提一下,你当前的组件代码里有两处小错误:

  1. ngOnInit里的item.userId应该是this.item.userId,否则会报未定义错误;
  2. map(user => username)应该是map(user => user.name),不然无法正确获取用户名。

修正后的组件代码:

ngOnInit() {
  this.store.dispatch(new LoadUser({id: this.item.userId}));
  this.username$ = this.store$.select(selectUser, {id: this.item.userId}).pipe(
    filter(user => !!user),
    map(user => user.name)
  );
}

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.04.30 15:17:43