Angular Async Pipe

Oct 2nd, 2020 - written by Kimserey with .

The async pipe is used in Angular to unwrap a value from an asynchronous primitive. An asynchronous primitive can either be in a form of an observable or a promise. It is one of the most important pipes of Angular as it allows us to remove the complexity of handling subscription to data coming from asynchronous sources like HTTP clients. The code of the async pipe is surprisingly simple and understanding it will allow us to avoid traps like the default null value. So in today’s post, we will reimplement the async pipe in a toned down version of it.

Async Pipe Usage

To illustrate the utility of the async pipe, we start by creating a simple component accepting a Profile as input:

1
2
3
4
5
6
7
8
9
10
11
12
export interface Profile {
  name: string;
  age: number;
}

@Component({
  selector: 'app-profile',
  template: ` <div></div> `,
})
export class ProfileComponent {
  @Input() profile: Profile;
}

which we can take use from our App component assuming that the profile comes from an observable; an asynchronous source:

1
<app-profile [profile]="profile$ | async"></app-profile>
1
2
3
4
5
6
7
export class AppComponent implements OnInit {
  profile$: Observable<Profile>;

  ngOnInit(): void {
    this.profile$ = of({ name: 'kim', age: 10 });
  }
}

We use of to simulate an observable with a single value but this would generally come from a http client call or even a redux implementation like the excellent ngrx store implementation.

Edge Cases

As we see in Profile, the template displays profile.name which is susceptible to null reference exception if profile is null. The obvious issue would occur if we just don’t pass any input:

1
<app-profile></app-profile>

or if we never assign any value to profile$ from the AppComponent:

1
2
3
export class AppComponent implements OnInit {
  profile$: Observable<Profile>;
}

In those cases, the value of profile would simply be null which we can easily prevent by never forgetting to assign a value to [profile] and never forget to assign the observable in OnInit.

But there is another scenario where profile can be null, and it is when the observable set to profile$ does not have any initial value. To illustrate that, let’s replace the observable by a delayed observable:

1
2
3
4
5
6
7
8
9
10
11
12
export class AppComponent implements OnInit {
  profile$: Observable<Profile>;
  profileSubject = new Subject<Profile>();

  ngOnInit(): void {
    this.profile$ = this.profileSubject.asObservable();
  }

  pushProfile() {
    this.profileSubject.next({ name: 'kim', age: 10 }};
  }
}

In this particular scenario, profile$ observes the profileSubject which has no initial value and will only have one value when pushProfile is invoked. In that particular case, the initial value will be null. That value will not be the result of the assignment of [profile] nor the result of the observable profile$ having a null value, but instead would be the result of the AsyncPipe returning null when there isn’t any initial value on the observable.

To prevent that, we can either use a *ngIf guard,

1
2
3
<ng-container *ngIf="profile$ | async as profile">
  <app-profile [profile]="profile"></app-profile>
</ng-container>

this works because *ngIf would guard against the actual result of the async pipe. Another way would be to can make sure we have an initial value with startWith for the observable,

1
this.profile$ = this.profileSubject.asObservable().pipe(startWith({ name: 'kim', age: 10 }));

or with a BehaviorSubject if the observables come from a subject.

1
profileSubject = new BehaviorSubject<Profile>({ name: 'kim', age: 10 });

Reimplementing Async Pipe

The last edge case we just discussed is more intricate than the others as it is a behaviour set by the framework rather than something controlled by the user. To understand where this behaviour come from, we can reimplement the async pipe defined in @angular/core:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
import {
  Pipe,
  OnDestroy,
  PipeTransform,
  ChangeDetectorRef,
  WrappedValue,
} from '@angular/core';
import { Observable, SubscriptionLike } from 'rxjs';

@Pipe({ name: 'asyncTwo', pure: false })
export class AsyncTwoPipe implements OnDestroy, PipeTransform {
  private _latestValue: any = null;
  private _latestReturnedValue: any = null;

  private _subscription: SubscriptionLike = null;
  private _obj: Observable<any> = null;

  constructor(private _ref: ChangeDetectorRef) {}

  ngOnDestroy(): void {
    if (this._subscription) {
      this._dispose();
    }
  }

  transform(obj: Observable<any>): any {
    if (!this._obj) {
      if (obj) {
        this._subscribe(obj);
      }
      this._latestReturnedValue = this._latestValue;
      return this._latestValue;
    }

    if (obj !== this._obj) {
      this._dispose();
      return this.transform(obj as any);
    }

    if (this._latestValue === this._latestReturnedValue) {
      return this._latestReturnedValue;
    }

    this._latestReturnedValue = this._latestValue;
    return WrappedValue.wrap(this._latestValue);
  }

  private _subscribe(obj: Observable<any>): void {
    this._obj = obj;
    this._subscription = this._obj.subscribe((value: Object) => {
      this._updateLatestValue(obj, value);
    });
  }

  private _dispose(): void {
    this._latestValue = null;
    this._latestReturnedValue = null;
    this._subscription = null;
    this._obj = null;
  }

  private _updateLatestValue(async: any, value: Object): void {
    if (async === this._obj) {
      this._latestValue = value;
      this._ref.markForCheck();
    }
  }
}

which can be used just like the async pipe:

1
<app-profile [profile]="profile$ | asyncTwo"></app-profile>

Here I am recreating the async pipe, naming it asyncTwo, where I toned down the implementation by removing the handling of promises. The main function being transform which would take the observable as input and return the latestValue being set by the callback set on this._obj.subcribe in _subscribe.

One interesting point to note is that the asynchronousy is handled by setting _latestValue = value in _updateLatestValue, then using ChangeDetectorRef.markForCheck to mark the view as changed, combined with pure: false from the Pipe directive to specify that the pipe should be checked on each change detection cycle, which will then result to a call of transform and return of _latestValue on the next change detection cycle.

Back to the topic of null on initial value, we can see the following handling:

1
2
3
4
5
6
7
if (!this._obj) {
    if (obj) {
        this._subscribe(obj);
    }
    this._latestReturnedValue = this._latestValue;
    return this._latestValue;
}

which makes it obvious, when we subscribe to the observable, if the observable has an initial value, this._latestValue will be set, else if the observable has no initial value, the first value returned by the pipe will be null. And that concludes today’s post!

Conclusion

In today’s post, we looked into the async pipe, we started by looking at how it could be used, then we moved on to look a few edge cases where the result of the async pipe could be null value. We then drill deeper into a specific edge case which is introduced by the framework rather than driven by the usage of the directive which is the initial value of an observable. And we looked at the reason why the framework was behaving this way by reimplementing the async pipe. I hope you now have a better understanding of the behaviour behind the pipe, see you on the next post!

External Sources

Designed, built and maintained by Kimserey Lam.