NGRX Effects / RXJS: Loop API Calls and Actions

Viewed 587

The goal is to loop through an Array of IDs and for each ID make an API Call and dispatch an Action with the result.

My current idea:

someEffect$ = createEffect(() => this.actions$.pipe(
    ofType(someAction),
    withLatestFrom(this.store.select(selectIds)),
    switchMap(([_, ids]) => {
      return forkJoin(ids.map(id =>
        this.service.getData(id))).pipe(
        map(result => {
          return actionSuccess({result});
        }),
        catchError((error: HttpErrorResponse) => {
          return of(actionError({error}));
        })
      )
    })
  ));

This kinda works, but has 2 unwanted behaviours:

  1. actionSuccess gets only dispatched once and with the result of all api calls as an array. I want one dispatch for each API Call.

Replacing forkJoin with merge triggers an action for each ID, but then the API Calls are no longer happening, so there has to be more to it.

  1. forkJoin waits for all API Calls to finish and only then continues. I want to dispatch actionSuccess immediately after every time getData() finishes.
3 Answers

I know you are asking how to dispatch actions looping through an array in the effects but, how about if, for example, let the component handle the looping operation, with that, you will be dispatching for each id a success action.

Component:

loadCustomerById(): void {
  this.store.select(ClientSelectors.selectIds).subscribe((ids) => {
    if (ids.length) {
      for (const id of ids) {
       this.store.dispatch(ClientActions.loadCustomerById({ payload: id }));
      }
    }
  })
}

Effects:

loadCustomerById$ = createEffect(() => this.actions$.pipe(
    ofType(loadCustomerById),  
    // you already get the id from action
    // no need of withLastestFrom  
    mergeMap(({ payload: id }) => 
      this.service.getData(id).pipe(
        map((response) => ClientActions.loadCustomerByIdSuccess({ payload: response })),
        catchError((error) => of(ClientActions.loadCustomerByIdFail({ error })))
      )
    ) 
  ));

Use merge instead of forkJoin - for more info see https://timdeschryver.dev/snippets#multiple-service-calls-from-an-effect

refresh$ = createEffect(() =>
  this.actions$.pipe(
    ofType(CustomerActions.refresh),
    exhaustMap(({ customerIds }) =>
      merge(
        ...ids.map((id) =>
          this.customersService.getCustomer(id).pipe(
            map(CustomerActions.getCustomerSuccess),
            catchError((err) =>
              of(CustomerActions.getCustomerFailed(id, err.message)),
            ),
          ),
        ),
      ),
    ),
  ),
)

I'd suggest converting the Observable of the Array into an Observable which emits all of the elements in the Array using from:

someEffect$ = createEffect(() => this.actions$.pipe(
  ofType(someAction),
  withLatestFrom(this.store.select(selectIds)),
  switchMap(([_, ids]) => from(ids)),
  mergeMap(id => this.service.getData(id).pipe(
    map(result => actionSuccess({ result })),
    catchError(error => of(actionError({ error })))
  ))
));

Depending on the length of ids, however, this could very quickly become untenable, and it might be worth refactoring your backend to accept a batch call.

Related