Handling data with the Angular AsyncPipe

In my current project at work, we're using Angular to build the front end of a web application that gives the user a dashboard of useful information. As part of this we've adopted a few patterns for handling data that I thought would be useful to others.

Asynchronous data sources

Say we want to get a random person from randomuser.me, so that we can show their first name. We might start with a service that looks like:

@Injectable()
export class RandomUserService {
  constructor(private http: Http) { }

  getRandomUser() {
    return this.http
      .get('https://randomuser.me/api/')
      .map(response => response.json().results[0]);
  }
}

In our component we then .subscribe to the resulting observable and assign the data to a field:

@Component({
  selector: 'random-user',
  template: '<span>{{ user.name.first }}</span>',
})
export class RandomUserComponent {
  user;

  constructor(private service: RandomUserService) { }

  ngOnInit() {
    this.service.getRandomUser()
      .subscribe(user => this.user = user);
  }
}

However, you quickly run into issues; prior to the GET call resolving via the subscription, this.user is null, so getting the appropriate fields from it fails:

TypeError: Cannot read property 'name' of null ...

and the view doesn't render at all.

Simple solutions

To avoid this, you could set a default value, so that the appropriate fields always exist:

user = { name: { first: 'Alice' } };

However, it's not always obvious what an appropriate default would be, and if the request fails the end user is potentially stuck looking at some dummy data.

Alternatively we can use the safe navigation operator to resolve each field:

<span>{{ user?.name?.first }}</span>

but that's not too neat and is prone to human error.

Leveraging observables

Instead, we can use the AsyncPipe to resolve the value asynchronously from the service:

<span>{{ firstName$ | async }}</span>

(The convention of a dollar sign suffix to indicate an observable was apparently popularised by Cycle.js.)

In the component, this can be implemented as a property:

get firstName$() {
  return this.service.randomUser$
    .map(user => user.name.first);
}

In the service, we've been using a "public observable, private subject" pattern:

@Injectable()
export class RandomUserService {
  private userSubject = new ReplaySubject(1);

  randomUser$ = this.userSubject.asObservable();

  constructor(private http: Http) { }

  fetchRandomUser() {
    this.http
      .get('https://randomuser.me/api/')
      .map(response => response.json().results[0])
      .subscribe(user => this.userSubject.next(user));
  }
}

Using a ReplaySubject means that new subscribers, joining after a fetch, still get the latest value. Keeping it private means that the subscribers can't push new data into it, so we know any new state must come from within the service itself. Any component can trigger a new request via the public fetch... method, and all subscribers then get the newest data as it arrives.

Combining data sources

In a few cases, we want to combine multiple requests (for example, to draw a graph using data from two API endpoints). Initially this seemed quite tricky, as we wouldn't necessarily know when both requests had resolved. However, RxJS provides combineLatest for this purpose:

ngOnInit() {
  this.combinedData$ = Observable.combineLatest(
    service.someData$,
    service.otherData$,
    (some, other) => this.combine(some, other)  // or "this.combine.bind(this)"
  );
}

See e.g. RxMarbles for a demonstration of what this operator does, as well as other operators that can be applied to your streams of data.

Gotchas

Note a few problems we've run into:

  • ExpressionChangedAfterItHasBeenCheckedError on anything that passes NaN into | async; I opened an issue about this and there's a pull request to fix it.
  • Exposing error states with a ReplaySubject can lead to some weird behaviour; use a vanilla Subject instead, to avoid errors getting replayed later.

For more information on testing services and components written in this way, see the follow-up article Testing async data in Angular.

Comments !