fe / rxjs / js

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.

Links

Next