Antomor logo

Antomor's personal website.

Where ideas become words (maybe)

Angular and Rxjs Http Requests: Common scenarios

How to perform http requests using rxjs; single, parallel and mixed

5-Minute Read

Sunrise in a wood

Any modern web application needs soon or later to perform some http request to retrieve data. Here below I’ll describe some common scenario and how to perform such requests using RxJS.

Single request

The most common scenario, no special rxjs handling, since that 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 to apply word-by-word the solution described by point 2, 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 could have never used it, so let’s do an example.

Our friend is following a run on tv, but we are not fans, so we are not interested in the run winner; the only thing we want to know is when the run 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. Practically it receives an array of observables and then we can subscribe to an array of results having the same array index of the requests.

Here is the resulting code:

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

But …

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

Here is the forkJoin code snippet that successfully finish 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, we have the results filled differently according with the responses status, and if some request fails, we have undefined in the corresponding item among the results. So if the 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 two perform 2 requests:

  • mergeMap: the second request starts as soon as possible, without waiting the end of the first one, so the order of the subscription isn’t guaranteed (if important, it must be manually handled 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 requests 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 maybe already know, performing a subscription within a subscribe is a very bad practice and it MUST BE absolutely avoided. This looks very similar to the callback hell problem, nowadays almost forgotten fortunately.

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 would compose the response by using both the first response and the responses coming from the 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 fail should be handled singularly
  2. If we have one request that uses a response of another request it’s likely that we need switchMap
  3. NO Subscribe-in-Subscribe

Resources

comments powered by Disqus

Recent Posts

Categories

About

Software Engineer passionate about Security and Privacy. Nature and animals lover. Sports (running, yoga, boxing) practitioner.