Safe Navigation Operator, RxJS and Async Pipe tinkering
Learn how to use the async pipe to write elegant, RxJS powered async code
6 min read
6 min read
If you already played with Angular, I’m pretty sure you came across RxJS. It plays a big role in Angular, especially in Http, Forms, Async Pipes, Routing and also in application architecture patterns like ngrx/store.
Rob Wormald’s (Angular Developer Evangelist @ Google) showed some impressive usage of RxJS with Angular during his talk at NgEurope on “Angular & RxJS”. Some involved using the Safe Navigation Operator and how it can be replaced via async pipes.
Let’s assume we have the following data returned by some server API.
{
"name": "Juri Strumpflohner",
"status": "Currently coding on a screencast",
"website": {
"url": "http://juristr.com",
"name": "juristr.com"
},
"twitter": {
"url": "https://twitter.com/juristr",
"name": "@juristr"
}
}
We want to create a component that displays the details of this person by using the data it gets passed. Something like this:
import {Component, Input} from '@angular/core'
@Component({
selector: 'person-detail',
template: `
<div>
{% raw %}Name: {{ person.name }}<br/>
Twitter: {{ person.twitter.name }}{%endraw%}
</div>
`,
})
export class PeopleComponent {
@Input() person;
}
So our first option is to simply get the data in our parent component via the Angular http
service.
this.http
.get('person.json')
.map(res => res.json())
.subscribe(data => {
this.person = data
});
And then pass the person
into our <person-detail>
component.
import { Subject } from 'rxjs/Subscription';
@Component({
selector: 'my-app',
template: `
<div>
<person-detail [person]="person"></person-detail>
</div>
`,
})
export class App implements OnInit, OnDestroy {
subscription: Subscription;
person;
constructor(private http:Http) { }
ngOnInit() {
this.subscription =
this.http
.get('person.json')
.map(res => res.json())
.subscribe(data => {
this.person = data
});
}
ngOnDestroy() {
// unsubscribe to avoid memory leaks
this.subscription.unsubscribe();
}
}
But wait. This won’t work. Look at our PersonDetailComponent
template:
<div>
Name: {% raw %}{{ person.name }}{%endraw%}<br/>
Twitter: {% raw %}{{ person.twitter.name }}{%endraw%}
</div>
We access name
and twitter.name
on person
, however the latter won’t be defined when our component is instantiated. After all, we first need to call the server to get the data. So this results in a runtime exception.
Error: Uncaught (in promise): Error: Error in ./PersonDetailComponent class PersonDetailComponent - inline template:1:9 caused by: Cannot read property 'name' of undefined
TypeError: Cannot read property 'name' of undefined
at _View_PersonDetailComponent0.detectChangesInternal (VM3423 component.ngfactory.js:45)
at _View_PersonDetailComponent0.AppView.detectChanges (core.umd.js:9305)
at _View_PersonDetailComponent0.DebugAppView.detectChanges (core.umd.js:9410)
at _View_App0.AppView.detectViewChildrenChanges (core.umd.js:9331)
...
By using the Safe Navigation Operator (?
) we can change our PersonDetailComponent
template to this:
<div>
Name: {% raw %}{{ person?.name }}{%endraw%}<br/>
Twitter: {% raw %}{{ person?.twitter.name }}{%endraw%}
</div>
Simply speaking, this special operator allows us to bind data to our template that will be available later. When the data becomes available, values evaluated and rebound via change tracking.
First of all, Pipes are what you may know as “filters” from Angular 1.x. Just as the original Unix pipes, they allow you to pass data through it and do something with it, such as transform the data for instance. Here’s an example of how a pipe that formats a date value could look like:
{%raw%}{{ someDateValue | format: 'dd/MM/yyyy' }}{%endraw%}
There’s a special, built-in pipe, called async
. The async pipe accepts an RxJs Observable
object and does the entire subscription and handling for us.
So we can basically transform our original example to this:
@Component(...)
export class App {
person;
constructor(private http:Http) { }
ngOnInit() {
this.person = this.http
.get('person.json')
.map(res => res.json());
}
}
Note how there is no more subscribe(...)
part but instead we directly assign the returned Observable
to our person
variable. But who does the subscription then?? It’s the async
pipe.
Our parent component (or smart component) remains unchanged, while our detail (or dumb component) displaying the person must be changed. Given the passed @Input
person is an Observable
we need to wrap it with the async pipe: (person | async)?.name
.
@Component({
selector: 'person-detail',
template: `
<div>
Name: {%raw%}{{ (person | async)?.name }}{%endraw%}<br/>
Twitter: {%raw%}{{ (person | async)?.twitter.name }}{%endraw%}
</div>
`,
})
export class PersonDetailComponent {
@Input() person;
...
}
Try it out yourself.
I’m not that big of a fan of the previous variant, where our dumb component visualizing the detail of our person needs to know about the async nature of it’s input. That creates coupling to the outside world. Within our dumb component I don’t want to know where my data comes from; its responsibility is mainly to visualize the input.
So we can do better. Rather than using the async pipe in our dumb component, let’s move it out to our parent.
@Component({
selector: 'my-app',
template: `
<div>
<person-detail [person]="person | async"></person-detail>
</div>
`,
})
export class App { ... }
Our dumb component’s template can be left without the async
wrapper.
This is much nicer in my opinion. Also, note that a big advantage of using the built-in async pipe is that we don’t have to deal with the Observable subscription/unsubscription any more by ourself.
There’s one thing left which we would change as well. Our dumb component still uses the “safe navigation operator”:
import {Component, NgModule, Input} from '@angular/core'
@Component({
selector: 'person-detail',
template: `
<div>
Name: {%raw%}{{ person?.name }}{%endraw%}<br/>
Twitter: {%raw%}{{ person?.twitter.name }}{%endraw%}
</div>
`,
})
export class PersonDetailComponent {
@Input() person;
}
Obviously we can totally live with that, but there’s another options as well by setting some default values on our @Input
. Let’s explore.
Version 2:
Don’t use the safe navigation operator, but rather do some default initialization of your @Input
object.
import {Component, NgModule, Input} from '@angular/core'
@Component({
selector: 'person-detail',
template: `
<div>
Name: {%raw%}{{ person.name }}{%endraw%}<br/>
Twitter: {%raw%}{{ person.twitter.name }}{%endraw%}
</div>
`,
})
export class PersonDetailComponent {
@Input() person;
ngOnInit() {
// set default init -> MUST BE IN ngOnInit
this.person = { twitter: {} };
}
}
Note, for some (to me) unknown reason, this has to be done in the ngOnInit
lifecycle event, otherwise it doesn’t work.
Async pipes work even more nicely. Our detail component gets the data and doesn’t have to neither use the safe navigation operator, nor default values. The reason is that our *ngFor
serves as a guard until the data arrives.
import {Component, NgModule, Input} from '@angular/core'
@Component({
selector: 'my-people',
template: `
<div *ngFor="let person of people">
{%raw%}{{ person.twitter.name }}{%endraw%}
</div>
`,
})
export class PeopleComponent {
@Input() people;
constructor() {
}
}
The async
pipe is a really powerful operator.
Don’t forget to check out Rob’s talk.
Many thanks to Brecht Billiet and [Dominic Elm](https://twitter.com/elmd) for reviewing this article._