If anything in your app happens asynchronously, an RxJS Observable will make your life easier. While this might sound like an ad for just about any JavaScript library ever created, the RxJS Observable has a few unique advantages that make it stand out. This blog post will explore those advantages using real-life RxJS examples.
But First, Some Theory
Everything I've ever wanted to teach about Functional Reactive Programming can fit into a quote from this excellent article (which you should read; I cannot recommend it enough):
Reactive programming is programming with asynchronous data streams.
The reasons why an RxJS Observable is preferable over anything in other JavaScript libraries are the following:
- An Observable is just the Observer pattern with a jetpack.
- The RxJS library is well-known and widely used.
- An Observable allows you to handle different asynchronous events, from a single finite operation (like HTTP request) to multiple repeatable actions (like keystrokes or cursor movements). There's a unified API for both.
- You can join, mix, transform, and filter different Observables with one API.
- RxJS Observables are already used with the most popular frameworks and libraries, such as Angular (where it's built-in) or React/Redux (
redux-observable
).
RxJS Observable Examples
How to Handle Events with Observables
Let's program something simple and compare how it's written in vanilla JavaScript with how it's written in RxJS 7. We're programming a button. When the button is clicked, it will generate and display a quasi-random string. Here's what that looks like in vanilla JS:
const button = document.querySelector('button');
const output = document.querySelector('output');
button.addEventListener('click', e => {
output.textContent = Math.random().toString(36).slice(2);
});
Here's what that looks like with an Observable:
const output = document.querySelector("output");
const button = document.querySelector("button");
fromEvent(button, "click").subscribe(() => {
output.textContent = Math.random().toString(36).slice(2);
});
You've probably noticed that the RxJS code doesn't seem to make things much easier. It's even a little longer than the vanilla implementation. So why bother? Well, let's add another feature to our app. We only want every third click to generate a random string.
fromEvent(button, "click")
.pipe(bufferCount(3)) // <--------- only added this line!
.subscribe(() => {
output.textContent = Math.random().toString(36).slice(2);
});
What would that look like in vanilla JavaScript? I'll leave that to you as an exercise, but I can assure you it won't be that easy.
The Power of Operators
Operators are where the true power of RxJS lies. RxJS has many operators and they give you lots of possibilities. In the above example, I used the bufferCount
operator, which collects (buffers) three events and then emits one event with an array of the buffered events.
But our example was way too easy. Let's complicate things. Let's program our app so it only generates a random string when users click the button three times within a very short period of time (400 ms). A triple click.
const click$ = fromEvent(button, "click");
click$
.pipe(
bufferWhen(() => click$.pipe(delay(400))), // <--------- during 400ms
filter((events) => events.length >= 3) // <-------- 3 or more events
)
.subscribe(() => {
output.textContent = Math.random().toString(36).slice(2);
});
Okay, let's explain what just happened. The bufferWhen
operator buffers all clicks until a function inside of it emits something. The function inside emits its first event 400 ms after the initial click. This means that bufferWhen
emits an array with all the clicks within 400 ms after the initial click. filter
is then used to only emit anything if there were three or more clicks.
Brilliant, isn't it?
Use Any Library with RxJS
The above only demonstrates one way of creating Observables, using the fromEvent
method. There are many more. You could, for example, automatically transform any Promise to an Observable with from
. There are also useful bindCallback
and bindNodeCallback
methods.
These methods allow you to use any other library with RxJS, greatly expanding its usefulness.
Handle HTTP Requests Smoothly
Another superpower of RxJS is that it can smoothly handle HTTP requests. Consider a scenario where we have an app that needs to fetch a list of albums and render it. I'll use the jsonplaceholder API for this example:
const albumsApiUrl = `https://jsonplaceholder.typicode.com/albums`;
Rx.Observable
.ajax(albumsApiUrl)
.subscribe(
res => console.log(res),
err => console.error(err)
);
The second parameter in the subscribe
function is used to handle errors. Let's now connect this with our previous example. With a click, our app will fetch the albums of a random user.
fromEvent(button, "click")
.pipe(mergeMap(getAlbums))
.subscribe(render, err => console.error(err));
function getAlbums() {
const userId = Math.round(Math.random() * 10);
return fromFetch(
`https://jsonplaceholder.typicode.com/albums?userId=${userId}`
).pipe(mergeMap(res => res.json()));
}
The mergeMap
operator is one of the most useful operators in RxJS. For every click, it calls getAlbums
and waits for the results. Let's see that in action:
It works, but with a caveat. If you click the button quickly a few times, the app will re-render its results. Not a nice user experience. Even worse, you can't be sure which request was actually resolved as the last one. This is known as the race condition in computer programming.
Solving this problem with vanilla JS is not trivial. You're looking to keep just the last request while unsubscribing to all previous requests. Ideally, even cancel previous requests. Can RxJS help with this?
You bet it can. There's an operator for that! Thanks to switchMap
, only the response to the last request will render. All previous requests will be canceled. This is incredibly powerful. Let's implement this in our example:
Let's Combine Some Observables
Time for the grand finale. We want to give our users the ability to type in a user ID, (input
) and select the type of resource they want to display (select
). But, the request can only be issued after both fields are filled in. After that, the app should automatically re-render whenever any of the fields changes.
(I remember implementing exactly this functionality for a client some time ago)
To make our code a bit cleaner, let's add event listeners and map events to values, so we can make the rest of our code simpler:
const id$ = fromEvent(input, "input")
.pipe(map((e) => e.target.value));
const resource$ = fromEvent(select, "change")
.pipe(map((e) => e.target.value));
We use the $
suffix because it's common practice to name variables this way when they hold Observables. Yes, this might throw you back to the jQuery era.
Next, we need to combine Observables so that, whenever one of them changes, we get the last values from both. Guess what? There's an operator for that. In fact, there are a number of operators for that. But let's use combineLatestWith
:
id$
.pipe(
combineLatestWith(resource$),
switchMap(getResources)
)
.subscribe(render);
In Summary
Whether or not you should use Observables ultimately comes down to whether you enjoy using them. If you do, dig deeper by reading The introduction to Reactive Programming you've been missing. It's a great article that cleared up many things for me.
If, on the other hand, you don't enjoy using RxJS Observables, I still recommend you get familiar with the concept, because programmers are using it more and more.
Bonus
Replace id$
with the code below and see what happens.
const id$ = fromEvent(input, "input")
.pipe(
map(e => e.target.value),
filter(id => id >= 1 && id <= 10),
distinctUntilChanged(),
debounceTime(200),
);