RxJS in less than 5 mins
RxJS is a wonderful library, unfortunately the documentation — while really detailed — is quite dense and can appear cryptic and overwhelming to a newcomer, making the learning experience way more frustrating than it should be.
In this introduction we’ll try to cover the core ideas. In particular, we’ll explore the library’s foundation blocks such as subscribe
, Observable
, Observer
, pipe
and the operators
to better understand the benefit and purpose of RxJS.
The main idea
In the js world, we like the array APIs and we would like something similar for the rest of the stuff we usually do.
We like these APIs because they make the code easier to read, test and reason about.
In particular, we like them because they help avoid nested functions and facilitate using pure functions instead (benefit of referential transparency).
Array APIs Examples:
// without using the array APIs
let data = [0, 1, 2, 3, 3, 4, 5,7, 9, 9, 9];
let evenTotal = 0;
for (let i = 0; i < data.length; i++) {
if (data[i] % 2 === 0) {
evenTotal = evenTotal + data[i];
}
}
// Using the array APIs
let data = [0, 1, 2, 3, 3, 4, 5, 7, 9, 9, 9];
let isEven = (x) => x % 2 === 0;
let sum = (partialSum, x) => partialSum + x;
let evenTotal = data.filter(isEven).reduce(sum, 0);
This is what RxJS allows you to do: have an array like APIs to do a little bit of anything. Its speciality: handling asynchronous events as they were collections.
Subscribe
The most important method in RxJS is subscribe
.
In a sense, you can think of it as similar to then
in a Promise
or addEventListener
in an HTMLElement
, but with steroids.
Some Examples:
myPromise
.then(() => console.log('promise resolved'));
document
.addEventListener('keyup', () => console.log('typings'));
rxjsObservable
.subscribe(() => console.log('a value change'));
let’s rewrite the first two cases with RxJS
from(myPromise)
.subscribe(() => console.log('promise resolved'));
fromEvent(document, 'keyup')
.subscribe(() => console.log('typings'));
RxJS to handle asynchronous events uses a push protocol. Push in the sense that the Observable pushes data to the Observer. The Observer is passive and receives data only when the Observable produces it.
As your intuition may suggest, subscribe
, then
and addEventListener
are functions that get called when something happens.
For example, then
gets called when a Promise
resolves, addEventListener
gets called when an Event
gets fired. Following the same principles, subscribe
gets called when an Observable
pushes data.
As arguments subscribe
can accept a single function
, as we saw in the examples — in this case, the function represents the next
function — or, as an alternative, it can receive an object
with three callbacks: next
, error
or complete
.
The next
callback is called when the Observable
pushes data, while the error
callback is called when the Observable
encounters an error, and lastly the complete
callback is called to notify that the data pushing is done.
from(myPromise)
.subscribe({
next: () => console.log('promise resolved'),
error: () => console.log('promise failed'),
complete: () => console.log('promise done'),
});
This {next, error, complete}
object is called Observer
. Despite having a name, it’s nothing but an object with 3 callbacks.
Observable
subscribe
is a method of a class called Observable
.
You can have an Observable
from a lot of sources.
Here is a list of possible sources:
- fetch
- promises
- events
- web sockets
- iterables
- arrays
- normal variables
- other observable
This is not a complete list, but it gives you an idea.
As mentioned, you can have an Observable
from a little bit of anything.
Don’t worry if some make sense while others are a little bit more puzzling — that’s normal.
Like: why do I need an observable for an array? can’t I just use an array? or, why do I need an observable for normal values? These are all valid questions.
The main reason is pipe
, which is the main difference between Promise
or Event
and the reason why you may want to use an Observable
instead.
Pipe
What is pipe
you may ask? Well, that’s another method provided by an Observable
.
But still, you may ask, and what is for? Do you remember when we said we like arrays APIs? With an array you may have something similar to [].filter().reduce()
.
You may think of pipe
as the function that allows filter
and reduce
in an Observable
.
The function that pipe
allows to use are called operators
.
RxJS has hundreds of them.
You can think of Operators as the Arrays’ map, filter, reduce, every, etc for Observables.
Operators example:
// possible partial search input implementation
import { fromEvent, throttle, interval } from 'rxjs';
let inputEl = document.querySelector('input');
fromEvent(inputEl, 'input')
.pipe(
map(e => e?.target?.value),
filter(searchTerm => searchTerm.length > 2),
debounceTime(500),
distinctUntilChanged()
)
.subscribe(() => console.log(inputEl.value));
map
, filter
, debounceTime
, distinctUntilChanged
are some of the hundreds of operators we were talking about before.
Without digging too much into the details, You can see how pipe
allows the chaining of RxJS operators before subscribe
gets hit. Try to imagine how you would have handled even just a debounce with addEventListener
.
Note as well how each operator accepts pure functions as arguments (eg: searchTerm => searchTerm.length > 2
).
Summary
In this introduction, we saw how Observables
extends Promises
and Events
(among others) and how pipe
allows the use of operators
which allows the use of pure functions.
With these core concepts in mind, you should be able to look through the official docs. Consider starting from Glossary and Overview as these sections will support you in further refining your mental model of RxJS and better absorb the rest of the docs.