RxJS is cool and increasingly popular, but it seems like there’s a fair bit of confusion around when and why you would use it. “Why would you ever need that?”, your coworkers might say to you.
I’m no RxJS expert and I don’t think it’s needed for every project, but for me, the main reasons to use it are that it allows you to:
Easily compose multiple events into one.
Declaratively transform the value emitted by an event sequence in a functional, lodash-esque style.
Better model asynchronous data flow. Keyboard input, AJAX requests, events from an event emitter, etc. are all asynchronous and produce values over time. Representing that with the same interface has a lot of value.
Let’s look at an example that demonstrates those bullet points.
Show me teh codez
So we want to create an event emitter that emits true/false when a user’s internet goes online/offline. Quite a few applications have this functionality, so it seems like a good example.
The requirements of what we’re trying to create is:
call the provided callback initially with the current online/offline status
emit true/false when online/offline
support multiple subscriptions (aka multiple callbacks registered to the event)
support unsubscribing a subscription
unsubscribe from underlying event sources if there are no subscriptions
don’t subscribe to event sources unless there’s a subscription
We’ll do the vanilla JS version first. The caller of this event emitter might look like this:
const onlineEmitter = createOnlineEmitter();
let unsubscribeFn = onlineEmitter(
isOnline => console.log(isOnline)
);
The callback function gets called initially with value of isOnline
and every time it changes after.
The vanilla version is a little long and somewhat complex due to all the bookkeeping you have to do.
function createOnlineEmitter() { | |
let cbs = []; //array of registered callbacks for the event | |
let unsub; //function for removing the main event listener | |
//this is the main event listener that gets registered with window.online/offline event | |
const mainListener = (isOnline) => { | |
//call all the subscribed callbacks | |
cbs.forEach(cb => cb(isOnline)); | |
}; | |
const registerMainListener = () => { | |
const boundOnline = mainListener.bind(null, true); | |
const boundOffline = mainListener.bind(null, false); | |
window.addEventListener('online', boundOnline); | |
window.addEventListener('offline', boundOffline); | |
//return unsubcribe functionality in a closure | |
return function unsubscribe() { | |
window.removeEventListener('online', boundOnline); | |
window.removeEventListener('offline', boundOffline); | |
}; | |
}; | |
const addCb = (cb) => { | |
cbs.push(cb); | |
//register main listener only once | |
//use existence of `unsub` as indicator if main event listener is added or not | |
if(!unsub) { | |
unsub = registerMainListener(); | |
} | |
}; | |
const removeCb = (cb) => { | |
const index = cbs.indexOf(cb); | |
if(index > -1) { | |
cbs.splice(index, 1); | |
} | |
//if no callbacks left, remove main event listener | |
if(cbs.length === 0 && unsub) { | |
unsub(); | |
unsub = null; | |
} | |
}; | |
return function initOnlineEmitter(cb) { | |
addCb(cb); | |
//call it async with the initial val | |
setTimeout(() => { | |
cb(navigator.onLine); | |
}); | |
//return unsubscribe function to caller | |
return removeCb.bind(null, cb); | |
}; | |
} | |
// implement it | |
const onlineEmitter = createOnlineEmitter(); | |
let unsub = onlineEmitter(isOnline => console.log(isOnline)); | |
unsub(); |
While I like the vanilla version, and it functionally does the same thing that the RxJS version will do below, it’s main drawback is that it’s not composable or flexible at all. It does it’s one thing and that’s it.
How does that look in RxJS?
The caller of the RxJS version will look something like this:
const online$ = createOnline$();
online$.subscribe(isOnline => console.log(isOnline));
//callback function has same behavior as the vanilla version
So similar to the vanilla one, but not exactly the same. Here’s the implementation:
const { Observable } = require('rxjs/Observable'); | |
require('rxjs/add/observable/fromEvent'); | |
require('rxjs/add/operator/map'); | |
require('rxjs/add/observable/merge'); | |
function createOnline$() { | |
//merge several events into one | |
return Observable.merge( | |
//use .map() to transform the returned Event type into a true/false value | |
Observable.fromEvent(window, 'offline').map(() => false), | |
Observable.fromEvent(window, 'online').map(() => true), | |
//start the stream with the current online status | |
Observable.create(sub => { | |
sub.next(navigator.onLine); | |
sub.complete(); //this one only emits once, so now we end it | |
}) | |
); | |
} | |
// implement it | |
const onlineSub = createOnline$().subscribe(isOnline => console.log(isOnline)); | |
onlineSub.unsubscribe(); |
So, quite a bit smaller, which is to be expected.
But to demonstrate the composability advantages of the RxJS version that would be harder to achieve with the vanilla:
const { Observable } = require('rxjs/Observable'); | |
require('rxjs/add/operator/filter'); | |
require('rxjs/add/operator/switchMap'); | |
require('rxjs/add/operator/take'); | |
require('rxjs/add/operator/toPromise'); | |
const axios = require('axios'); | |
const online$ = createOnline$(); | |
//only make the network request when we're online | |
//the request will simply get queued up until then | |
function requestWhenOnline(ajaxPromiseFn) { | |
return online$ | |
.filter(online => online) | |
//we only reach this point when we're online | |
.switchMap(ajaxPromiseFn) //instead of emitting true/false, emit the value of this function | |
.take(1) //this ensures the observable ends after we've gotten a response | |
.toPromise(); //convert it all to a promise | |
} | |
requestWhenOnline(() => axios.get('http://example.com')) | |
.then(console.log) | |
.catch(console.error); |
That’s a simple example but there’s a lot of possibilities because Observables can so easily be composed and combined together to form new ones. Hopefully this demonstrated some of the value they can bring to an application.
Here’s a plunkr of the examples above: RxJS vs vanilla comparison - Plunker
(To play around with the online/offline event, open Chrome’s dev tools, go to Network, and toggle the “Offline” checkbox).