Skip to main content
  1. Blog/

Angular and Rxjs Http Requests: Common scenarios

·938 words·5 mins
Antonio Morrone
Author
Antonio Morrone
Staff/Tech Lead Software Engineer. Building software since 2013, in blockchain protocols & Web3 infrastructure since 2021. Security-minded. Remote from Italy.

Any modern web application needs, sooner or later, to perform some http requests to retrieve data. Below, I’ll describe some common scenarios and how to perform such requests using RxJS.

Single request
#

The most common scenario, no special rxjs handling needed, since Angular provides an Http service that returns an Observable.

services.getItems().subscribe();

Is that easy? Yes. In the particular example, I assumed the http service call was inside the service.getItems method, but it could be a fetch or anything else returning an observable.

Parallel requests
#

In this scenario instead, we want to perform multiple parallel requests. A common example could be:

  1. We perform an http request that returns a list of items
  2. For each item, we need to perform another request to fill some item detail

No — if you are thinking of applying the solution described in point 2 word-for-word, it is not the correct one, even if it can seem logically feasible.

!!Bad code!!

responses = []
items.forEach((item, index) => {
  service.get(item.id).subscribe( (response) => {
    responses[index] = response;
  });
});

Why not? Because we cannot control the requests, we can’t know when all of them will be resolved and what is their state.

Our goal can be achieved by using the forkJoin operator. It is often compared to Promises.all(), but many of you may never have used it, so let’s do an example.

Our friend is watching a race on TV, but we are not fans, so we are not interested in who wins; the only thing we want to know is when the race is finished, nothing else (we don’t watch TV, we want to have a beer). With forkJoin we wait for all the runners to finish. In practice, it receives an array of observables, and we can then subscribe to an array of results having the same array index as the requests.

Here is the resulting code:

itemRequests = items.map((item) => service.get(item.id));
forkJoin(itemRequests).subscribe((results) => {
  // do something with results
})

But …

This leads to a common mistake. What if a runner is disqualified? The race is still valid for the remaining runners, right? Well, by default, forkJoin succeeds only if all the runners successfully finish their run, and this isn’t what we usually want.

Here is the forkJoin code snippet that succeeds even if one of the requests fails:

itemRequests = items.map((item) => {
  return service.get(item.id).pipe(
    // not the best way to handle the error,
    // maybe we could retry, or set some placeholder instead
    catchError((err) => of(undefined));
  )
});
forkJoin(itemRequests).subscribe((results) => {
  // do something with results
})

In that way, the results are filled differently according to the responses’ status, and if a request fails, we have undefined in the corresponding item among the results. So if request[1] fails, results[1] will be undefined.

Sequential requests
#

For sequential requests there are many operators that can be used to achieve almost the same result, but they have slightly different behaviours.

Assuming we want to perform 2 requests:

  • mergeMap: the second request starts as soon as possible, without waiting for the end of the first one, so the order of the subscriptions isn’t guaranteed (if important, it must be handled manually in some way)
  • concatMap: the second request starts only after the end of the first one and the emitted results will maintain the order of the subscriptions. So the first result corresponds to the first subscription and so on.
  • switchMap: it is slightly different, because it executes the inner subscription only after the first one, but, in particular, only one request at a time will be active. One big advantage of using this operator is that on each emission, it can cancel existing requests.

Single request followed by parallel requests
#

After analysing both single and multiple requests, we can compose them.

!!Bad code, don’t use it

// get the items
service.getItems().subscribe((items) => {
  itemRequests = items.map((item) => {
    return service.get(item.id).pipe(
      // not the best way to handle the error,
      // maybe we could retry, or set some placeholder instead
      catchError((err) => of(undefined));
    );
  });
  forkJoin(itemRequests).subscribe((results) => {
    // do something with results
  })  
});

As you may already know, performing a subscription within a subscribe is very bad practice and MUST be absolutely avoided. This looks very similar to the callback hell problem, nowadays fortunately almost forgotten.

What is the right way to compose them? By using our wonderful switchMap operator.

// get the items
service.getItems().pipe(
  switchMap((items) => {
    // it must return an observable
    itemRequests = items.map((item) => service.get(item.id).pipe(
        // not the best way to handle the error,
        // maybe we could retry, or set some placeholder instead
        catchError((err) => of(undefined));
    ));
    return forkJoin(itemRequests);
  }),
  tap((itemRequestsResponses) => {
    // here we have the array of responses returned from forkJoin
  })
);

If we wanted to compose the response using both the first response and the responses coming from forkJoin:

// get the items
service.getItems().pipe(
  switchMap((items) => {
    // it must return an observable
    itemRequests = items.map((item) => service.get(item.id).pipe(
        // not the best way to handle the error,
        // maybe we could retry, or set some placeholder instead
        catchError((err) => of(undefined));
    ));
    return forkJoin(itemRequests).pipe(
      map((itemRequests) => {
        // compose the response in some way using both itemRequests and items
      })
    );
  }),
  tap((ourComposedResponse) => {
    // here we have the object composed in the inner map of the forkJoin
  })
);

Conclusions
#

Take Away
#

  1. forkJoin performs parallel requests, but each failure should be handled individually
  2. If we have a request that uses the response of another request, it’s likely that we need switchMap
  3. NO Subscribe-in-Subscribe

Resources
#