Canceling RxJS Observables With ngOnDestroy In Angular 2 Beta 6
I happen to love Promises. Like anything else, they can be complicated at first. But, once you understand the core promise chain concepts, they can be quite enjoyable to reason about. That said, a sore point for Promises has always been their lack of cancelability. This leads to some interesting design patterns where you have to "protect" your promise callbacks from context-dependent changes. Because of this hoop-jumping, the most attractive RxJS feature that I've seen so far is the simple ability to cancel (aka, unsubscribe, aka, dispose) an RxJS stream before it completes. This makes it super easy to dispose of pending streams in something like the Angular 2 component life-cycle method, ngOnDestroy.
Run this demo in my JavaScript Demos project on GitHub.
When you teardown an Angular 2 component, you need to make sure that you also teardown any pending requests and timers. This way, you don't run into a situation where you're processing data or triggering errors for interfaces that are no longer relevant to the user.
NOTE: This is a concept, not a law. There are most certainly times when you do want to trigger an error message even after an interface has been destroyed. This is especially true if the interface was destroyed due to an "optimistic" action that did not wait for data to return from the remote API.
With RxJS streams, when we subscribe to a stream, we are given a subscription object. This subscription object provides a method for unsubscribing from the observable stream. This method doesn't trigger a "complete" event or an "error" event. Rather, when you unsubscribe from the stream, your stream callbacks stop being invoked entirely. This is really nice!
To explore this idea, I created a small demo that toggles between two lists of people: Friends and Enemies. Each of these lists is represented by an Angular 2 component that receives its data from a common PeopleService that returns RxJS streams. The Friend data fetch always results in a successful outcome whereas the Enemy data fetch always results in an error. All error messages in the demo are represented as an alert() modal. But, we'll be unsubscribing from the RxJS streams as we destroy the components so as not to show alert() modals for user interfaces that are no longer relevant to the user.
In the following code, the FriendList and the EnemyList are almost identical. I am keeping them separate so that I can actually destroy a component (and leverage the ngOnDestroy component life-cycle method) as I toggle back and forth between the two lists.
<!doctype html>
<html>
<head>
<meta charset="utf-8" />
<title>
Canceling RxJS Observables With ngOnDestroy In Angular 2 Beta 6
</title>
<link rel="stylesheet" type="text/css" href="./demo.css"></link>
</head>
<body>
<h1>
Canceling RxJS Observables With ngOnDestroy In Angular 2 Beta 6
</h1>
<my-app>
Loading...
</my-app>
<!-- Load demo scripts. -->
<script type="text/javascript" src="../../vendor/angularjs-2-beta/6/es6-shim.min.js"></script>
<script type="text/javascript" src="../../vendor/angularjs-2-beta/6/Rx.umd.min.js"></script>
<script type="text/javascript" src="../../vendor/angularjs-2-beta/6/angular2-polyfills.min.js"></script>
<script type="text/javascript" src="../../vendor/angularjs-2-beta/6/angular2-all.umd.js"></script>
<!-- AlmondJS - minimal implementation of RequireJS. -->
<script type="text/javascript" src="../../vendor/angularjs-2-beta/6/almond.js"></script>
<script type="text/javascript">
// Defer bootstrapping until all of the components have been declared.
// --
// NOTE: Not all components have to be required here since they will be
// implicitly required by other components.
requirejs(
[ "AppComponent", "PeopleService" ],
function run( AppComponent, PeopleService ) {
ng.platform.browser.bootstrap(
AppComponent,
[
PeopleService
]
);
}
);
// --------------------------------------------------------------------------- //
// --------------------------------------------------------------------------- //
// I provide the root App component.
define(
"AppComponent",
function registerAppComponent() {
var FriendList = require( "FriendList" );
var EnemyList = require( "EnemyList" );
// Configure the App component definition.
ng.core
.Component({
selector: "my-app",
directives: [ FriendList, EnemyList ],
// Both the friend-list and the enemy-list components know how to
// load their own internal data. The point of this exercise is to
// see how to cancel that underlying data fetch as we toggle
// back-and-forth between the two components.
template:
`
<p>
<a (click)="showList( 'friends' )">Friends</a>
|
<a (click)="showList( 'enemies' )">Enemies</a>
</p>
<div [ngSwitch]="list">
<friend-list *ngSwitchWhen=" 'friends' "></friend-list>
<enemy-list *ngSwitchWhen=" 'enemies' "></enemy-list>
</div>
`
})
.Class({
constructor: AppController
})
;
return( AppController );
// I control the App component.
function AppController() {
var vm = this;
// Determine which list is being rendered.
vm.list = "friends";
// Expose the public methods.
vm.showList = showList;
// ---
// PUBLIC METHODS.
// ---
// I switch over to rendering the given list.
function showList( newList ) {
vm.list = newList;
}
}
}
);
// --------------------------------------------------------------------------- //
// --------------------------------------------------------------------------- //
// I provide a list of friends. Rather than accepting Inputs, this component
// knows how to go fetch its down data.
// --
// NOTE: The FriendList and the EnemyList could have probably been combined
// and parameterized with an Input; however, I wanted to have two distinct
// components so that I could actually destroy one when I switch rendered lists.
define(
"FriendList",
function registerFriendList() {
var PeopleService = require( "PeopleService" );
ng.core
.Component({
selector: "friend-list",
template:
`
<h2>
Friends
</h2>
<p *ngIf="! friends">
<em>Loading...</em>
</p>
<div *ngIf="friends">
<p>
Oh sweet, you have {{ friends.length }} friends!
</p>
<ul>
<li *ngFor="#friend of friends">
{{ friend.name }}
</li>
</ul>
</div>
`
})
.Class({
constructor: FriendListController,
// Register the directive life-cycle methods on the prototype
// so that they will be picked-up at runtime.
ngOnInit: function noop() {},
ngOnDestroy: function noop() {}
})
;
FriendListController.parameters = [ new ng.core.Inject( PeopleService ) ];
return( FriendListController );
// I control the FriendList component.
function FriendListController( peopleService ) {
var vm = this;
// I hold the latest subscription to the RxJS Observable stream that
// is being used to access the friends. This will provide us with a
// means to "cancel" the sequence if the user exits out of the
// component before the sequence has completed.
var peopleSubscription = null;
// I hold the collection of friends to render.
vm.friends = null;
// Expose the public methods.
vm.ngOnDestroy = ngOnDestroy;
vm.ngOnInit = ngOnInit;
// ---
// PUBLIC METHODS.
// ---
// I get called when the component has been unmounted. This provides
// the perfect hook to cancel any pending timers or requests.
function ngOnDestroy() {
console.warn( "Destroying friend-list" );
console.info( "Canceling any hot Rx subscriptions." );
// If the component had a chance to initiate the request for
// friend data, we need "cancel" the sequence. What we don't want
// is something like the Error handler to show an Alert() modal
// after the component has been destroyed (at least not in our
// particular use-case). When we unsubscribe, it will prevent
// the Value, Error, and Complete handlers from being invoked.
// Even if the sequence had already completed, this will be safe
// to call.
if ( peopleSubscription ) {
peopleSubscription.unsubscribe();
}
}
// I get called after the component has been instantiated and the
// inputs have been bound.
function ngOnInit() {
peopleSubscription = peopleService.getFriends()
.subscribe(
function handleValue( friends ) {
vm.friends = friends;
},
function handleError( error ) {
console.error( "Error in getFriends():", error );
alert( "Oops: We couldn't load friend data." );
},
function handleComplete() {
console.debug( "Completed getFriends()." );
}
)
;
}
}
}
);
// --------------------------------------------------------------------------- //
// --------------------------------------------------------------------------- //
// I provide a list of enemies. Rather than accepting Inputs, this component
// knows how to go fetch its down data.
// --
// NOTE: The FriendList and the EnemyList could have probably been combined
// and parameterized with an Input; however, I wanted to have two distinct
// components so that I could actually destroy one when I switch rendered lists.
define(
"EnemyList",
function registerEnemyList() {
var PeopleService = require( "PeopleService" );
ng.core
.Component({
selector: "enemy-list",
template:
`
<h2>
Enemies
</h2>
<p *ngIf="! enemies">
<em>Loading...</em>
</p>
<div *ngIf="enemies">
<p>
Oh chickens, you have {{ enemies.length }} enemies!
</p>
<ul>
<li *ngFor="#enemy of enemies">
{{ enemy.name }}
</li>
</ul>
</div>
`
})
.Class({
constructor: EnemyListController,
// Register the directive life-cycle methods on the prototype
// so that they will be picked-up at runtime.
ngOnInit: function noop() {},
ngOnDestroy: function noop() {}
})
;
EnemyListController.parameters = [ new ng.core.Inject( PeopleService ) ];
return( EnemyListController );
// I control the EnemyList component.
function EnemyListController( peopleService ) {
var vm = this;
// I hold the latest subscription to the RxJS Observable stream that
// is being used to access the enemies. This will provide us with a
// means to "cancel" the sequence if the user exits out of the
// component before the sequence has completed.
var peopleSubscription = null;
// I hold the collection of enemies to render.
vm.enemies = null;
// Expose the public methods.
vm.ngOnDestroy = ngOnDestroy;
vm.ngOnInit = ngOnInit;
// ---
// PUBLIC METHODS.
// ---
// I get called when the component has been unmounted. This provides
// the perfect hook to cancel any pending timers or requests.
function ngOnDestroy() {
console.warn( "Destroying enemy-list" );
console.info( "Canceling any hot Rx subscriptions." );
// If the component had a chance to initiate the request for
// enemy data, we need "cancel" the sequence. What we don't want
// is something like the Error handler to show an Alert() modal
// after the component has been destroyed (at least not in our
// particular use-case). When we unsubscribe, it will prevent
// the Value, Error, and Complete handlers from being invoked.
// Even if the sequence had already completed, this will be safe
// to call.
if ( peopleSubscription ) {
peopleSubscription.unsubscribe();
}
}
// I get called after the component has been instantiated and the
// inputs have been bound.
function ngOnInit() {
peopleSubscription = peopleService.getEnemies()
.subscribe(
function handleValue( enemies ) {
vm.enemies = enemies;
},
function handleError( error ) {
console.error( "Error in getEnemies():", error );
alert( "Oops: We couldn't load enemy data." );
},
function handleComplete() {
console.debug( "Completed getEnemies()." );
}
)
;
}
}
}
);
// --------------------------------------------------------------------------- //
// --------------------------------------------------------------------------- //
// I provide access to the people repository. People collections are returned
// as RxJS Observable streams.
define(
"PeopleService",
function registerPeopleService() {
return( PeopleService );
// I provide access to the people repository.
function PeopleService() {
var simulatedNetworkLatency = ( 2 * 1000 ); // In milliseconds.
// Return the public API.
return({
getEnemies: getEnemies,
getFriends: getFriends
});
// ---
// PUBLIC METHODS.
// ---
// I return the collection of enemies (as a stream).
function getEnemies() {
// NOTE: This will throw an error "in the future" after the
// simulated network activity has completed.
var stream = Rx.Observable
.timer( simulatedNetworkLatency )
.concat( Rx.Observable.throw( new Error( "Server Error" ) ) )
.last()
;
return( stream );
}
// I return the collection of friends (as a stream).
function getFriends() {
var friends = [
{
id: 1,
name: "Sarah"
},
{
id: 2,
name: "Kim"
},
{
id: 3,
name: "Joanna"
}
];
return( Rx.Observable.of( friends ).delay( simulatedNetworkLatency ) );
}
}
}
);
</script>
</body>
</html>
As you can see, I initiate the data fetch in the ngOnInit life-cycle method and then hold onto the subscription object returned by the data fetch request. This way, when we navigate away from the Enemy list before it has a chance to load, we successfully avoid showing the error modal:
So far, for me, this is the selling point for RxJS streams. Of all the things that I have seen in RxJS so far (which albeit is quite limited), this is the one feature that catches my attention. Sure, retries, buffering, and emitting unique values are "nice" features; but, those type of use-cases don't come up that often in the apps that I build. Being able to cancel a data request, on the other hand, is a problem that I have to deal with on a daily basis.
Want to use code from this post? Check out the license.
Reader Comments
Great article as always.
I wonder how the mechanism is when using the "async" pipe in the template.
Imho this is a nice construction, so you don't have to do a myObservable.subscribe..
But I'm curious wether it's necessary to unsubscribe when using async.. and if it is, how to do so..
@Lars,
Ok I just did some investigation.... turns out the async pipe automatically calls unsubscribe from an observable, when the component is destroyed.. nice!
@Lars,
Thanks for digging into that. I haven't really done anything with the async pipe yet. I look at the docs. But, to be honest, I am not sure I have a really good use-case for it. Meaning, how often do you have an async value that results in the *one thing* you want to output? Meaning, my async values are usually objects or arrays that are used to render a number of parts of the page, not just a single {{ ... | async }} binding.
If you have a use-case in mind, I'd love to hear it.
@Ben,
Agree, the async pipe is best for lists of data.
The beautiful thing is that you don't have to worry about unsubscribing to streams on component destruction.
About the one object data....
I often expose an Observable containg one value ( a BehaviourSubject). For example in my wrapper for the http service, I expose a "pending" observable so subscribers can see if there are pending http requests..
This is used in components that visualize that something is going on.. spinners etc.. in the template for those, I just do an *ngIf="pending | async" so it is turned on automagically depending on the status of the observable.
It's really just syntactical sugar to avoid doing explicit subscribe/unsubscribe in your component code.
@Lars,
I really like the idea of being able to expose an Observable as an indicator of something else. When I was fooling around with an HTTP client, I did exactly the same thing - exposed a "pendingCommands" observable. To be able to take that and just pipe (to so speak) into the View is very interesting.
I like your articles, really!
But, man, please, do something with code indentation, it's horrible :(
@Beverly,
To each their own ;)