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 UsageTo 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.
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 });
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!
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!