I've spent the day diving into RxJS with Angular2, as the practice of modeling user interfaces as streams is new to me.
I'm experimenting with a user service that provides a stream of User objects. The first User
object will be provided when the user is authenticated. Additional User
Objects may be provided when the User is updated, e.g. they update their profile. If the user is not logged when the application loads, or they logout, then null
is emitted.
As such, an observer implementation in a component would look like this:
export class AppComponent {
private user: User;
private showLoginForm: boolean;
constructor(public userService: UserService) { }
ngOnInit() {
this.userService.user$.subscribe(user => {
this.user = user;
this.showLoginForm = this.user ? false : true;
})
}
}
The userService.user$
observable is a BehaviorSubject
type. Is this how you would implement this? The idea of sending null
, to a stream that expects a User
object, isn't sitting right with me. But at the same time, it does provide a convenient way to answer the question: is the User available or not?
"Going reactive" really needs to be an all-or-nothing thing, IMO. This is a really good recent article on this topic in general.
With regard to Angular2 apps specifically, what this means is that you want to model things as streams everywhere, from end to end - from the HTTP responses that deliver data to the templates used to display it.
So in your case, rather than:
@Component({
template: ` name: {{ user?.name }` //elvis operator always needed with this approach
})
export class AppComponent {
private user: User; // breaks the chain
ngOnInit() {
this.userService.user$.subscribe(user => {
this.user = user;
})
}
}
you'd want to do something like:
@Component({
template: ` name: {{ (user$ | async).name }` //let angular deal with that shit
})
export class AppComponent {
private user$: Observable<User>; // stream :)
private showLoginForm$: Observable<boolean>;
ngOnInit() {
this.user$ = this.userService.user$; //could actually be done in constructor
this.showLoginForm$ = this.user$.map(user => !user) //i.e. user ? false : true
}
}
The key thing to note here is you're modeling your application state as a stream all the way from the service (which presumably is relaying an observable API response) to the component to the template, where you leverage AsyncPipe
to let angular deal with all the dirtywork of subscribing and updating the UI to reflect changes as needed.
In response to @MarkRajcok's comment:
Speaking of subscribe()... don't you need one on your last line of ngOnInit()?
No, and that's actually an important point. The beauty of AsyncPipe
is that you don't have to manually subscribe to anything, just let Angular do it for you. This sidesteps a minefield of potential change-detection problems that can arise from handling these things manually.
But how do you deal with errors? E.g., suppose you get an error when you try to get a user from the backend. If you want to use NgIf to either display an error or display the user, don't you have to manually subscribe() and "break the chain"?
Not necessarily. Observable.catch() is quite useful for these purposes:
@Component({
template: ` <div>name: {{ (user$ | async).name }</div>
<div *ngIf="hasError$ | async">ERROR :("></div>`
})
export class AppComponent {
private user$: Observable<User>;
private showLoginForm$: Observable<boolean>;
private hasError$: Observable<boolean>;
private error$: Observable<string>;
ngOnInit() {
this.user$ = this.userService.user$;
this.showLoginForm$ = this.user$.map(user => !user)
this.hasError$ = this.user$.catch(error => true).startWith(false);
this.error$ = this.user$.catch(error => error.message);
}
}
That being said, my message here isn't that it's never necessary to manually subscribe to things (of course there are situations when it is) but rather, that we should avoid doing it wherever possible. And the more comfortable I get with rx, the rarer I realize those situations are.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With