Thursday, March 28, 2024

Binding Asynchronous Data to Local Variables with RxJS

In the recent Asynchronous Processing in ES6  web development tutorial, we learned one way to process several asynchronous calls in a row using the static Promise.all() method. It combines an iterable of promises into a single promise that provides the amalgamated results as a single array. Since that article was published, several people have asked me if there was a pure RxJS alternative to accomplish the same thing that did not rely on promises. The answer, as we will see in this tutorial, is a resounding yes! While we are at it, we will be covering how to bind asynchronous data to a local variable, such as one that stores an ID, so that all information for a given entity is kept together. Otherwise, processing numerous RxJS calls within a single subscribe() callback can result in both nasty and insidious mangling of continuity. To demonstrate how and why that can happen, we will fetch a list of link labels twice, both with and without data binding, and witness the very different outcomes.

Before moving forward, you may want to revisit (or read) Asynchronous Processing in ES6.

Making Pure RxJS Asynchronous Calls within a Loop

The solution presented in the Asynchronous Processing in ES6 tutorial employed the Promise.all() method to test the validity of several links. Here is that method again, for the purposes of recapitulation:

public async hideTheBadLinks() {
  Promise.all(this.links.map(link => this.isValidLink(link)))
    .then(results => this.links = links.filter((_, i) => results[i]));
}

There is nothing wrong with the above code per se; it works exactly as intended. There is, however, no guard against memory leaks, such as we might unwittingly produce if we were to navigate to another page. In that case, the subscription would live on even after the component was destroyed, so that, navigating back to the page would again invoke the component constructor and spawn a new subscription.

Meanwhile, using RxJS operators comes with its own risks, if used incorrectly: mainly that asynchronous payloads could become dissociated from their related ids. To understand how that could occur, observe the following (flawed) solution:

private ngUnsubscribe = new Subject();

// create an array with values from 1 to 5
public linkIds = [...Array(5)].map((x,i) => i+1);
public linkNames: string[];

constructor() {
  from(this.linkIds)
    .pipe(
      mergeMap<number, Observable<string>>(id => this.getLinkName(id)),
      toArray<string>(),
      takeUntil(this.ngUnsubscribe)
    ).subscribe(linkNames => this.linkNames = linkNames);
}

public ngOnDestroy() {
  this.ngUnsubscribe.next();
  this.ngUnsubscribe.complete();
}

While the above solution ensures that our subscription ends when the component is destroyed, it introduces a whole new issue. The mergeMap operator doesn’t wait for the previous inner Observable to complete before triggering the next inner Observable, allowing multiple inner Observables to emit values in parallel. Under heavy load, or with rapid-fire calls, it’s possible for requests to be processed out of order!

A far better approach is to use forkJoin. It runs all observable sequences in parallel and collects their last elements. This means that the operator gets values from completed observables and returns a completed observable with single value. This both preserves the original order and makes unsubscribing unnecessary! Notice how much simpler the following implementation is compared to the last one:

forkJoin<Observable<string>[]>(
  this.linkIds.map<Observable<string>>(
    linkId => this.getLinkName(linkId)
  )
).subscribe(linkNames => this.linkNames = linkNames);

Read: RxJS Observables Primer in Angular

An Even Better Solution

By now it should be apparent that storing local ids and fetched associated data as separate parallel arrays is not the most robust approach. In asynchronous processing, object attributes should be kept together as much as possible. With that in mind, let’s refactor the forkJoin code to store both the link id and name in one object.

First, let’s define an interface that outlines the object structure:

interface Link {
  id: number;
  name: string;
}

We can now declare an array of Links:

public links: Link[];

Now to fetch the link names the CORRECT way, so that the ids and names are associated together. To do that, we’ll pipe the output of getLinkName() to the RxJS map operator. In addition to receiving the linkName as a parameter, the map’s callback function also has access to the linkId thanks to the closure formed by outer Array.map’s callback function:

forkJoin<Observable<Link>[]>(
  this.linkIds.map<Observable<Link>>(
    linkId => this.getLinkName(linkId)
      .pipe<Link>(
        map<string, Link>(linkName => ({
            id: linkId,
            name: linkName
          })
        )
      )
  )
).subscribe(links => this.links = links);

Read: Executing RxJS Observables in Order

Accessing Link Data in the Template

Storing link data in objects affects how we access it within the template. Previously, link names were associated with their ids using the loop index, which was passed to the getLinkInfo() method:

<ul>
  <li *ngFor="let linkName of linkNames; let i=index">
    <a role="button" (click)="getLinkInfo(i)" href="javascript:void(0)">
      {{linkName}}
    </a>  
  </li>
</ul>

Storing link data in objects eliminates the need for a loop index as we can pass the object directly to getLinkInfo(). The link name is accessible via the link object’s name attribute:

<ul>
  <li *ngFor="let link of links">
    <a role="button" (click)="getLinkInfo(link)" href="javascript:void(0)">
      {{link.name}}
    </a>  
  </li>
</ul>

Having all link attributes within the same object makes the getLinkInfo() method’s job much easier as well:

public getLinkInfo(link: Link) {
  this.linkInfo = 'You clicked the link with id '
                + link.id + ' and name "'
                + link.name + '".';

}

The Demo

On codepen.io, you’ll find a demo that puts the mergeMap and forkJoin RxJS solutions together for easy comparison. Watch what happens when we click one of the links in the first (bad) row:

RxJS merMap example

 

Due to the inclusion of link ids in their names, it’s easy to spot the incongruities. Were the ids not part of the link names, the mismatch could go on until someone realized something was amiss!

Final Thoughts on Binding Asynchronous Data with RxJS

In this tutorial we got a double whammy of RxJS goodness: how to process several asynchronous calls in a row using RxJS operators while binding fetched data to their ids, so that all information for a given entity is kept together.

Read more JavaScript programming and web development tutorials.

Rob Gravelle
Rob Gravelle
Rob Gravelle resides in Ottawa, Canada, and has been an IT guru for over 20 years. In that time, Rob has built systems for intelligence-related organizations such as Canada Border Services and various commercial businesses. In his spare time, Rob has become an accomplished music artist with several CDs and digital releases to his credit.

Get the Free Newsletter!

Subscribe to Developer Insider for top news, trends & analysis

Popular Articles

Featured