Search code examples
javascriptrxjsangular2-observablesrxjs-pipeable-operatorsrxjs-observables

Combining observables in series & parallel to fetch data from multiple APIs


I am trying to check the validity of a function I have written in Typescript, in congruence with RxJS observables, that fetches some bookings from one service and then for each booking fetches its corresponding location and activity from another service.

I am simply writing this post to verify the validity of what I have written and to ask if there is anything I could have done more efficiently.

let params = new HttpParams();
params = params.append('status', 'C');
params = params.append('offset', offset.toString());
params = params.append('limit', limit.toString());
return this.http.get(`${this.environment.booking.url}/my/bookings`, { params }).pipe(
    mergeMap((bookings: Booking[]) => {
        if(bookings.length > 0) {
            return forkJoin(
                bookings.map((booking: Booking) =>
                    forkJoin(
                        of(booking),
                        this.activityService.getActivity(booking.activity),
                  this.locationService.getLocation(booking.finalLocation),
                    ).pipe(
                        map((data: [ Booking, Activity, Location ]) => {
                            let booking = data[0];
                            booking.activityData = data[1];
                            booking.finalLocationData = data[2];
                            return booking;
                        })
                    )
                )
            )
        }

        return of([]);
    }),
    catchError((err: HttpErrorResponse) => throwError(err))
);

I am expecting for this function to return a list of bookings alongside their corresponding location and activity. However more importantly I want to verify that what I am doing is correct and sensible. Is there anything I could have done differently to make it cleaner/ more human-readable (not nit-picking, please 😁 )?

On a different note, that of performance, I also have a follow-up question with regards to performance. Given that a list of bookings has common activities and locations. Is there a way to only fetch activities and locations without any duplicate HTTP requests? Is this already handled under the hood by RxJS? Is there anything I could have done to make this function more efficient?


Solution

  • I'm not sure about the efficiency, but, at least for me, it was a little hard to read

    Here's how I'd do it:

    I used a dummy API, but I think it correlates with your situation

    const usersUrl = 'https://jsonplaceholder.typicode.com/users';
    const todosUrl = 'https://jsonplaceholder.typicode.com/todos';
    const userIds$ = of([1, 2, 3]); // Bookings' equivalent
    
    userIds$
      .pipe(
        filter(ids => ids.length !== 0),
        // Flatten the array so we can avoid another nesting level
        mergeMap(ids => from(ids)),
        // `concatMap` - the order matters!
        concatMap(
          id => forkJoin(ajax(`${usersUrl}/${id}`), ajax(`${todosUrl}/${id}`))
            .pipe(
              map(([user, todo]) => ({ id, user: user.response, todo: todo.response }))
            )
        ),
       toArray()
      )
      .subscribe(console.log)
    

    Here is a StackBlitz demo.

    With this in mind, here is how I'd adapt it to your problem:

    this.http.get(`${this.environment.booking.url}/my/bookings`, { params }).pipe(
        filter(bookings => bookings.length !== 0),
        // Get each booking individually
        mergeMap(bookings => from(bookings)),
        concatMap(
            b => forkJoin(
                this.activityService.getActivity(b.activity),
                this.locationService.getLocation(b.finalLocation),
            )
            .pipe(
                map(([activity, location]) => ({ ...b, activity, location }))
            )
        ),
        // Getting the bookings array again
        toArray()
        catchError((err: HttpErrorResponse) => throwError(err))
    );