Asynchronous programming
Angular Asynchronous Patterns RxJS Typescript Visual Studio Code

Using RxJS CombineLatest to Synchronize Dependent UI Data

In this blog I will show how to use the RxJs operator CombineLatest to synchronize loading of multiple data subscriptions from various data sources including Web APIs into an Angular UI form.

The most common approach we are familiar with is to use a basic RxJs Subscribe() and nest additional subscribers (inner subscribers) to retrieve data. What if the first subscriber does not have any data? In this case the inner subscriber will have no chance to retrieve any data.

A typical nested subscriber is shown:

this.api.getBook(this.id).subscribe((res: Book) => {
  this.book = res;  

  this.selectedGenre = this.book.genre;
  this.selectedGenre = this.selectedGenre.charAt(0).toUpperCase() + this.selectedGenre.slice(1);
      
  this.lookupService.getGenres().subscribe((gen: Genre[]) => {
    this.genres = gen.map(g => g.genre);
  });
      
  console.log("read book " + this.id + " from API");
});

In many cases the above nested subscriber would work where the data providers subscribed to were performing well. However, there are situations when incoming data being subscribed to would be delayed and would cause the remaining inner subscribers to wait on their outer subscriber(s). In the case where no data was provided due to some system, application, or network issue, the remaining inner subscribers would be deadlocked waiting to commence, and the form would show none, or incomplete data.

In summary, the above is essentially running the observables in sequential order, taking the sum of the durations of each observable – a highly inefficient method!

To avoid this situation, we use the RxJs CombineLatest() function which returns the latest value from each of the specified observables. You will never experience a situation where the remaining observables were left waiting! You will either get none, some results, or all. All observables will get a chance to retrieve their respective data in parallel and report the result through one subscription.

The total timespan being the duration of the longest observable.

The code to achieve this is shown below:

const getBook$ = this.api.getBook(this.id);
const getGenres$ = this.lookupService.getGenres();

combineLatest(
[
  getBook$, 
  getGenres$
])
.subscribe(([book, genres]) => {
  if (!book || !genres)
    return;
  console.log("number of genres  = " + genres.length);
  console.log("book name = " + book['title']);        

  this.genres = genres.map(g => g.genre);
  this.book = book;  
  this.selectedGenre = this.book.genre;
  this.selectedGenre = this.selectedGenre.charAt(0).toUpperCase() + this.selectedGenre.slice(1);
});

Notice I have included a logic gate to immediately return and avoid processing if either of the results is null. If some scenarios you may wish to also check the lengths of returned arrays are non-zero to ensure that processing of dependent data does not use null or empty data. In the above situation, the initial results for both book and genre would be null and would be immediately returned. Eventually when both are subscribed, they will both be non-null and able to be utilized.

In our respective API code, we have:

getBook(id): Observable<Book>
{
  return this.http.get<Book>('http://localhost/BookLoan.Catalog.API/api/Book/Details/' + id); 
}

In our service lookup code, we have:

getGenres(): Observable<Genre[]> 
{
  let arrGenres: Genre[] = [];
  arrGenres.push({id: 1, genre: "Childrens"});
  arrGenres.push({id: 2, genre: "Family sage"});
  arrGenres.push({id: 3, genre: "Fantasy"});
  arrGenres.push({id: 4, genre: "Fiction"});
  arrGenres.push({id: 5, genre: "Folklore"});
  arrGenres.push({id: 6, genre: "Horror"});
  arrGenres.push({id: 7, genre: "Mystery"});
  arrGenres.push({id: 8, genre: "Non-fiction"});
  arrGenres.push({id: 9, genre: "Sci-fi"}); 
  return of(arrGenres); 
}

The parameters of the CombineLatest() method are all observables as are our service and API methods above.

As we can see in the UI, the data for the various form UI controls is provided for by the dependent observables:

The most practical example is where we are loading data into a web form used for data entry.

Another example might be loading data in from various new feeds or summary data into different parts of a dashboard. We could then combine a BehaviorSubject observable to display progression for each data view.

The use of the CombineLatest observable is devised to achieve the three goals:

  1. Obtain results from multiple observables running in parallel.
  2. Avoid deadlocking that would be imposed from using nested inner subscribers.
  3. Higher performance than serially subscribing to observables.

That’s all for this post.

I hope you found it useful and informative.

Social media & sharing icons powered by UltimatelySocial