Reactive Programming with Angular by Example (Part 1)

Posted on Posted in Angular2

Web applications benefit a lot from reactive programming. The application reacts immediately when the user clicks, even if it takes a couple of second to load the data. In the early days, applications used to stall at this point. The computer froze, and you couldn’t say whether it had crashed or not. So developers invented the progress bar. That’s still freezing but in a more entertaining way.

Modern web applications do a lot better. They show the next page immediately, filling in data a bit later. That approach has many advantages. It gives the user immediate feedback. You can also load the top-most data first and load less often used data later. In most cases, this even means the user can continue their work earlier.

Let’s have a look how to do this with Angular. Reactive programming isn’t difficult, but if you’re not used to it, you have to learn to think outside the box.

Loading data asynchronously

Basically, all you have to do is to put your data in an observable. After that, you have to keep in mind that you never know whether the data is already there. Angular makes this particularly simple by using the RX.js framework and by offering the async pipe.

Let’s start with a simple example: displaying the user’s address. If you want to see the actual code, have a look at the GitHub repository of this article.

The naive approach

The simplest way to implement this is to rely on Angular’s change detection:

<dl>
  <dt>Street:</dt>
  <dd>{{userService.address?.street}}</dd>
  <dt>Zip code:</dt>
  <dd>{{userService.address?.zipcode}}</dd>
  <dt>City:</dt>
  <dd>{{userService.address?.city}}</dd>
</dl>

So far, there are few surprises. Angular’s best practices tell us that the application state should be stored in a service, so I’ve created a UserService loading and storing the address data. Let’s have a look at this class:

export interface Address {
  firstname: string;
  lastname: string;
  street: string;
  city: string;
  zipcode: string;
}

export class UserService {
  public address: Address;

  constructor(private http: Http) {
    http.get("https://example.com/rest/user/address")
      .map(response => response.json() as Address)
      .subscribe(address => { this.address = address; });
  }
}

What’s wrong with this approach?

The nice thing about this approach is that it works. Angular’s change detection mechanism manages to update the page as soon as the REST call returns the data. Before that, Angular renders the page, leaving the values delivered by the service blank. Notice that the page is rendered before the data arrives. That’s why the HTML template contains Elvis operators like {{address?.street}}. As long as the address is undefined or null, the street defaults to an empty string.

This simple and intuitive programming model made AngularJS 1.x such a great success. However, the general idea of Angular 2+ is to make things more explicit. Telling Angular (or the programmer) when the data changes is one such example.

The disadvantage of the naive approach is that Angular can’t know for sure when one of the attributes userService.address or userService.address.street change. It has to apply sophisticated heuristics to detect the changes reliably. The good news that Angular is every bit up to the task. But we can do a lot better. Using Observables, Angular can know for sure when to update a value. That’s more efficient than relying on change detection. Plus, the source code clearly indicates which values are going to change over time. That’s an important clue to the guy who has to maintain your code three years later.

Observables and async

Angular’s template language can deal with Observables directly. So instead of unwrapping the address in the subscribe callback, you can simply store the Observable returned by the REST call:

export class UserService {
  public address$: Observable<Address>;

  constructor(private http: Http) {
    this.address$ = http.get("https://example.com/rest/user/address")
      .map(response => response.json() as Address);
  }
}

Now we can use the async pipe to tell Angular about the Observable:

 
 <dt>Street:</dt>
 <dd>{{(userService.address$ | async)?.street}}</dd>
 <dt>Zip code:</dt>
 <dd>{{(userService.address$ | async)?.zipcode}}</dd>
 <dt>City:</dt>
 <dd>{{(userService.address$ | async)?.city}}</dd>

Note that in this particular case, we have to put the async pipe directly behind the variable address$. It looks a lot more elegant if we can put the async pipe at the end of the expression, but to achieve this we’d have to make the variable street an Observable. We’ll see in a minute how to simplify the syntax.

Cold and hot observables

Hot Observables start observing immediately. They are useful for watching continuous streams of events like mouse clicks, keystrokes, and websockets. REST calls are different. Usually, you want to call them a single time, and you’ll want to close the connection after receiving the server’s answer. So http.get() returns a cold Observable.

Cold observables are only activated after subscribing them. If there’s no subscriber, there’s no need to trigger the REST call. In our case, the subscription is hidden in the async pipe.

Actually, we’ve got three subscribers, one for each field of the address. According to Netanel Basal, this triggers three HTTP requests. For some reason, that’s not what the application did when I debugged it, but it did run the map() function several times. So you should be careful about this. Netanel suggests using Promises instead of Observables:

export class UserService {
  public address$: Promise<Address>;

  constructor(private http: Http) {
    this.address$ = http.get("https://example.com/rest/user/address")
      .map(response => response.json() as Address)
      .toPromise();
  }
}

However, it doesn’t seem to be a good idea to introduce a new paradigm just to prevent multiple REST calls. There are two other solutions which are often recommended. Following the naive approach prevents multiple calls very effectively. Another solution is to call share(). However, the best approach seems to be using the “*ngIf as” syntax.

*ngIf … as

This useful feature has been introduced with Angular 4. It solves two problems at once: it allows us to show a placeholder until the data has been loaded and the template syntax becomes simpler. No more putting async into parentheses:

 
<div *ngIf="userService.address$ | async as address; else loading">
  <dl>
    <dt>Street:</dt>
    <dd>{{address.street}}</dd>
    <dt>Zip code:</dt>
    <dd>{{address.zipcode}}</dd>
    <dt>City:</dt>
    <dd>{{address.city}}</dd>
  </dl>
</div>
<ng-template #loading let-user>Loading...</ng-template>

The “as address” bit assigns the real address to a variable. As long as the Observable doesn’t has a value, the variable is falsy, so the else clause applies. That’s the ng-template after the div.

By the way, sometimes you also find this variation of the code:

 
<div *ngIf="userService.address$|async; else loading; let address">

The as syntax has been implemented with this GitHub ticket to make the code more readable.

Defining your own directive storing a snapshot in a variable

Currently, Angular only allows you to store the snapshot of the observable in a variable using *ngIf. If you need an unconditional version, you can implement it yourself, as Oskar Karlsson and Alex Rickabaugh show.

Wrapping it up

So far we’ve seen how to load data asynchronously and how to display them. That’s the easy part. In the next part of this series, we’ll learn how to work with the data. How do you access the data when the user clicks a button? How to modify the data which isn’t stored anywhere, but exists only as a volatile stream of events? After that, we’ll learn how work with multiple Observables. For instance, how to call two REST calls simultaneously if you need the result of both to continue? How to call a REST call taking the result of another REST call as input?

Dig deeper

Stop using observables when you should use a promise
Cold vs. hot observables

Leave a Reply

Your email address will not be published.