If you've worked in the frontend JavaScript space, you may have heard, seen, or used RxJS.
But what is RxJS precisely?
Simply put, it's lodash for events.
We could extend that definition and say: "it's a library for reactive programming using Observables, following the Observer Pattern." But does that really help you understand what RxJS is and how to actually use it? Probably not.
You might also be asking, "Why is it useful?", and I would tell you that it makes asynchronous code easier to write, maintain, and reason about. But I doubt that's the most helpful description.
This article will attempt to give you an understanding of what RxJS is and how to get started with it.
It should be noted, however, that RxJS is an implementation of two programming paradigms that a lot of developers struggle to get their heads around. So, if you feel lost or confused when working with RxJS, don't fret. We've all been there.
Within this article, I will cover a basic overview of Reactive Programming and the Observer Pattern before detailing how to set up some simple use-cases in RxJS to give hands-on experience with using the library.
Reactive Programming
Reactive Programming is a programming paradigm wherein code is written to react to events that are produced from a data source or event emitter, such as a mouse click.
You may have heard of events before if you've worked with the DOM in JavaScript. A common event would be the click
event, which you would handle with an onClick
event handler. Something like the following:
<div onClick="handleClick()"></div>
But I think it's best to define an event as an action that is created asynchronously, such as when the user performs an action, or when an HTTP call receives a response.
Therefore, we can allude that Reactive Programming is all about handling events that are created as our application runs. It is a great method of handling applications that involve a lot of user interactivity, as we can react to the events that the user creates as they use the application.
This makes it an ideal candidate for frontend development. However, writing and maintaining an application that responds to events can quickly become cumbersome without a suitable pattern in place.
Observer Pattern
The Observer Pattern is a pattern that is usually used when implementing event handling systems, or any system that must react to events throughout the application lifecycle.
In its most basic form, it consists of subjects
and observers
, alternatively called publishers
and subscribers
.
A subject
is an object that consists of a stream of events, and a list of dependents. These dependents are the observers
.
When the subject's stream receives a new event, it tells each of its observers
that it has received a new event, and will send them a copy of the event.
The observers
will then handle this event in whatever fashion they see fit. In other words, what they were set up by the developer to do.
Let's review a quick example to help clarify this.
Let's say we need to implement some functionality that tracks user engagement within our application by keeping a record of how many times a user clicks, and by keeping a record of how much time has passed between each time the user has clicked.
With the Observer Pattern, not only can we do this very easily, but we can also separate the concerns, making it much easier to test and maintain, as well as allowing us to add or remove new ways of recording metrics based on the user's clicks within our application.
We would have a subject
that receives an event every time a user clicks within the application.
We would then set up two observers
. One that will update the record of how many times the user has clicked within the application, and one that will record the time between each click. It may seem like overkill to handle this with two different observers
but it massively reduces coupling of logic within our application.
Creating and Using an Event Stream with RxJS
Now, let's look at how we would implement the above use-case using RxJS. It's worth noting that RxJS has two methods of setting up an Event Stream, Subjects and Creation Operators
- Subjects
- Subjects share a single execution path to all of their observers
- You would normally set these up manually, and using their API, dictate what values get sent as the message to all of the observers.
- This will be covered in more detail in a future article!
- Creation Operators
- These are special functions that will help you turn almost anything into an Event Stream, allowing you to handle them in an asynchronous manner.
To implement the use-cases of performing logic, based on user clicks, within our application, there is a great Creation Operator called fromEvent
that creates a stream, and sends a new message to the observers of the stream every time the user clicks within our application.
Let's start by setting up the stream:
const userClick$ = fromEvent(document, 'click');
Something to note here is the $
suffix. This is a convention when writing async code to highlight that this property is an Observable
. Essentially, something that can be subscribed
to. We will cover Observables in more detail in a future article so stay tuned!
Now, let's see how we would keep track of how many times a user has clicked within our application using this Event Stream:
let numberOfClicks = 0;
const trackNumberClicksSubscription = userClick$.subscribe((event) => {
numberOfClicks++;
console.log(`User has clicked ${numberOfClicks} times!`);
});
Every time the user clicks in our app, the numberOfClicks
will increase by 1 and the total value will be logged to the console output.
User has clicked 1 times!
User has clicked 2 times!
User has clicked 3 times!
.subscribe()
returns a Subscription
.
This Subscription gives you the ability to unsubscribe
from an Event Stream if the need arrives. For example, if your application only cares about the user's first 10 clicks, then you could add logic to unsubscribe
from the Event Stream when numberOfClicks === 10
. This will aid performance as it prevents logic from being run when it isn't necessary.
Subscriptions will be covered in more depth in a future article!
let numberOfClicks = 0;
const trackNumberClicksSubscription = userClick$.subscribe((event) => {
numberOfClicks++;
console.log(`User has clicked ${numberOfClicks} times!`);
if(numberOfClicks === 10) {
trackNumberClicksSubscription.unsubscribe();
console.log(`No longer tracking number of user clicks!`);
}
});
User has clicked 9 times!
User has clicked 10 times!
No longer tracking number of user clicks!
Now, how about our other use-case, in which we track the time between each user click? Let's set up a new Subscription to the userClick$
Event Stream.
let lastClickTime = -1;
const trackTimeBetweenClicksSubscription = userClick$.subscribe((event) => {
// Wait until the first click has been received before we start tracking
// the time between each click
if (lastClickTime === -1) {
lastClickTime = Date.now();
return;
}
const difference = (Date.now() - lastClickTime) / 1000;
console.log(`Time between user clicks: ${difference}s`);
lastClickTime = Date.now();
});
We simply record the last time the user clicked, and check the difference between that time, and the time we receive the new click
event.
Date.now()
returns the epoch time in milliseconds, therefore we need to divide by 1000
to get the number of seconds between clicks.
We now have two Subscriptions that handle separate logic, allowing it to be easily maintained and tested!
You can see the above examples in action here.
Conclusion
We've barely scratched the surface of RxJS here, but hopefully by looking at the example above you can start to see the power it provides in writing and maintaining asynchronous, event-based code.
The real power of RxJS comes from its many Operators which can further extend its capabilities and use-cases. We'll cover that in a different article, so stay tuned!