Asynchronous programming
Angular Asynchronous Patterns RxJS Typescript Visual Studio Code

How to use RxJS CombineLatest to Synchronize Dependent UI Data

Welcome to today’s post.

In this post 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.

In a previous post, I showed how to synchronize data loading using BehaviorSubject. What we did with BehaviorSubject was to put a gate on the HTML rendering within the component template and prevent the user interface from rendering until the dependencies passed within the form and any necessary initializations are completed.

In this post, we are focusing on observable operators that are used within the form initialization lifecycle to help synchronize data for our form controls.

The most common approach we are familiar with is to use a basic RxJs Subscribe operator and nest additional subscribers (inner subscribers) to retrieve data.

In Angular forms where multiple sources of data being used to render reports, charts, and data present data from multiple tables within a data entry screen, the ability to synchronize data effectively allows the screen to render without noticeably impacting the end user experience.

Problems with Tasks within Nested Subscribe Operators

I will explain why, even though we can use the most obvious approach, nested subscribers can lead to unexpected results including situations where deadlocks can occur. I will then show how to efficiently combine data from observables efficiently.

There is at least one problem with the approach of using subscribe operators:  

One problem you may notice is when 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 significantly 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 their own task, and the form would show none, or incomplete data.

In summary, the above is essentially running the observables in sequential order, with total time taken being the sum of the durations of each observable – a highly inefficient method!

Solving Data Synchronization Issues by using CombineLatest

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, in 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.

Typical Observable Data Sources to Synchronize

The combining of observables can be applied to arrays that are generated by some expensive operation within the application, or even data sources that are returned from a web API. Below we have some typical examples of data returned within our application.

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, which returns an array, 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 this post useful and informative.

Social media & sharing icons powered by UltimatelySocial