Safe Navigation Operator, RxJS, and Async Pipe Tinkering
RxJS? Safe Navigation operator? Async pipes? Not sure what I'm talking about? In this article I'd like to explore some cool combination of Http, RxJS and Async Pipes.
Join the DZone community and get the full member experience.
Join For FreeIf 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 (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.
Our task
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>
Name: {{ person.name }}<br/>
Twitter: {{ person.twitter.name }}
</div>
`,
})
export class PeopleComponent {
@Input() person;
}
Option 1: Pass in a Person Object
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: {{ person.name }}<br/>
Twitter: {{ person.twitter.name }}
</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)
...
Safe Navigation Operator to the Rescue
By using the Safe Navigation Operator (?
) we can change our PersonDetailComponent
template to this:
<div>
Name: {{ person?.name }}<br/>
Twitter: {{ person?.twitter.name }}
</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 get evaluated and rebounded via change tracking.
Option 2: Using Async Pipes
First of all, Pipes are what you may know as “filters” from Angular 1.x. Just like 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:
{{ someDateValue | format: 'dd/MM/yyyy' }}
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.
Variant 1: Async Pipe in the Detail Component
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: <br/>
Twitter:
</div>
`,
})
export class PersonDetailComponent {
@Input() person;
...
}
Variant 2: Async Pipe in the Parent Component
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 its 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 by ourselves anymore.
Safe Navigation Operator vs. Default Values
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: {{ person?.name }}<br/>
Twitter: {{ person?.twitter.name }}
</div>
`,
})
export class PersonDetailComponent {
@Input() person;
}
Obviously, we can totally live with that, but there are other 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: {{ person.name }}<br/>
Twitter: {{ person.twitter.name }}
</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.
Option 3: Lists
Async pipes work even more nicely. Our detail component gets the data and uses neither 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">
{{ person.twitter.name }}
</div>
`,
})
export class PeopleComponent {
@Input() people;
constructor() {
}
}
Conclusion
The async
pipe is a really powerful operator.
- It deeply integrates with RxJS Observables and the Angular change detection mechanism.
- It handles Observable subscriptions transparently for us (in an optimized way).
- It makes our async code look as if was synchronous.
Don’t forget to check out Rob’s talk.
Published at DZone with permission of Juri Strumpflohner. See the original article here.
Opinions expressed by DZone contributors are their own.
Comments