Centric connect.engage.succeed

RxJS: reactive is usually more fun than passive

Geschreven door Martijn Kloosterman - 29 januari 2019

Martijn Kloosterman
Having started to toy around with Angular, you can’t help but take it apart to see what makes it tick. Exploring the GitHub repositories of the Angular project will make you feel like Indiana Jones entering a trap-laden tomb where there is sure to be some hidden treasure.

As well as providing support for a range of languages (JavaScript, C#, Java and many others) and a large API with various different functions for you to explore, it also forces you to start thinking in an declarative programming style. This will probably be the biggest shock to programmers who are not yet used to it.

Before we start on RxJS, let’s take a look at what imperative and declaractive means in the context of programming.

Imperative versus declarative

Basically, declarative programming (new, modern, all the cool kids do it) versus imperative programming (can get cluttered, harder to read) boils down to the difference between writing code that knows how to do something versus coded instructions saying what to do in specific cases.

Sometimes even an explanation doesn’t help that much, eh. Let’s see if a little example clarifies things.

Here is a simple JavaScript function that should take an array of numbers and return an array with each number squared: 

"use strict";
let myArray = [0, 1, 2, 3, 4, 5, 6];

function imperative(a) {
    let result = [];
    for (let i = 0; i < a.length; i++) {
        result.push(a[i] * a[i]);
    }
    return result;
}

function declarative(a) {
    return a.map(v => v * v);
}

console.log(imperative(myArray));
console.log(declarative(myArray));

The first function tells the program what to do. You start at the top and loop through the logic to end at the bottom with the final result.

The second function tells the program how to do something. It ultimately produces the same result, but the code doesn't have a flow; the essence of declarative. No additional plumbing is needed and it’s easier to read.

(F)RP is not an acronym for a genre of games, but it can be an adventure

‘Reactive programming is programming with asynchronous data streams.’ (1)

The functional part means that you can combine these streams, using one as input for the next, or combine several of them into one or more new ones. RxJS is an extension of this – it gives you observables in various flavours and a large collection of functions to use on them. It’s basically a toolbox with more tools than you’ll need for any one job – just pick the ones you need and ignore the rest.

You want to use it because it produces easy-to-read code, similar to C#’s LINQ. You’ll recognise the notation, which involves dot-chaining every action like this: array.doThis().doThat().doTheFinalThing(). Every output of the previous function is the input for the next.

A number of powerful JavaScript functions (map, filter, reduce) will automatically push you in the direction of functional programming, so it’s entirely possible that you’ve already been working reactively without even realising it.

Memorise and live these three. They are your bread and butter when working in JavaScript.

Moving on.

The explanations of declarative, imperative and reactive that I’ve given above are simplified, but these subjects go much deeper. There are many people smarter than me who have written books on these subjects, but that goes beyond the scope of this blog. Check the sources list at the end for further reading on the subject.

With that under your belt, let’s move on to RxJS!

Keep your eye on the Observable

Let’s start with the absolute basic building block: the Observable object (2). These are collections that may, at some indeterminable time in the future, possibly contain values. They expose three functions that you need to be aware of: Next, Error and Complete. This is an implementation of the generic Observer pattern. (3)

You can do various things with an Observable. For example, you can subscribe to it and wait for something to show up. You can combine several Observables and only do something when they all contain at least one value. You can map the Observable’s result to something else. That sounds pretty complex, but in reality, it behaves like an array and doesn’t trigger anything until it either gets a value, runs into an error or indicates that it’s completed doing whatever it was doing. In other words, the Next, Error and Completed events.

Let’s create a sandbox to play around in first

In order to demonstrate and play with RxJS, we’ll need to create a sandbox first. This isn’t overly complicated and only takes a few minutes. I assume that you are familiar with Git, NodeJS and NPM, and have them installed and ready. If you don’t, check my previous blogs for instructions.

Clone your own sandbox like this:

git clone https://github.com/Rarz/rxjs-demo.git
cd rxjs-demo
npm install
npm run start

That will download the sandbox, build it and start it.

Go to your favourite browser and head for the local webpack-dev-server, which you can find at http://localhost:8080/

Press F12 to open the dev console and F5 to refresh the page so you can see the logged results.

Go to the src\index.ts file. It will look like this, which is also the first example:

import { of } from 'rxjs';

const source = of(1, 2, 3, 4, 5);
const subscribe = source.subscribe(
  (x) => console.log(x),
  (err) => console.error(err),
  () => console.log('closed')
);

You will see something similar to this in the console window output:

The Observable is created by the ‘of’ and passes the values to the subscriber(s) in sequence. After it passes 5, there’s nothing left, so it signals ‘completed’.

The subscribe function takes three functions as variables, but only the first one is required: when a value is received, the second and third functions are optional. The second is called when an error is thrown and the third when the complete state is reached.

The Observable does not remember what it passed to its subscribers. It does not maintain state...If you want it to remember the last value set or all the values set, you can use a Subject instead. More on Subjects later.

Angular works with Observables a lot. Any HTTP GET or POST service call returns a (self-closing) Observable that you can subscribe to. The routing service can be subscribed to as well. EventEmitters are also part and parcel of connecting bits within Angular – subscribing to stuff is what makes Angular tick.

Subjects are smarter Observables and can remember stuff

A Subject is a specialised type of Observable – it comes with its own set of special tricks (4). The most important thing to know is that a Subject can remember what it sent to its subscribers, and thus send it again if asked to.

A Subject keeps a list of all the subscriptions and can produce this list on request with a call to connect() as a ConnectableObservable object. In combination with refCount(), you can then make the Observable start executing when the first subscriber arrives and stop when the last leaves.

There are a number of Subject types available for your use:

  • Subject: Remembers what the last value set was, but will return null to the very first subscriber if nothing was set.
  • BehaviorSubject: Remembers what the last value set was and must start with an initial value to return to the first subscriber.
  • ReplaySubject: Remembers what the last values set were and sends all of these to new subscribers. You can limit the collection by time or size and maximum number of items to return.
  • AsyncSubject: Only sends the last value received to the subscribers and only when the execution is complete. The subject is no longer available after this.

An example of a BehaviorSubject:

import { BehaviorSubject  } from 'rxjs';

const subject = new BehaviorSubject('Starting value');
subject.subscribe({
  next: (s) => console.log('Observer 1: ' + s)
});
subject.next('new value');
subject.subscribe({
  next: (s) => console.log('Observer 2: ' + s)
});
subject.next('final value');

Which results in this output:

 

Things to note: Observer 1 sees ‘starting value’, ‘new value’ and ‘final value’. Observer 2 joins after ‘new value’ was passed so sees that (because it was remembered) and ‘final value’. It never saw the ‘starting value’ string.

Operators operate

In this last section, I want to address the operators – this is where RxJS really shines. You get the choice of over 450 different operators that you can combine with your Observables to build your logic. (Although Rx supports a great many different programming languages, not all languages support all operators, so apart from the basic set, you might not have them available in the language of your choice.)

The operators can be divided into a number of categories:

  • Creating Observables
  • Transforming Observables
  • Filtering Observables
  • Combining Observables
  • Error handling operators
  • Observable utility operators
  • Conditional and Boolean operators
  • Mathematical and Aggregate operators
  • Backpressure operators
  • Connectable Observable operators
  • Converter operators

The sheer number of them is a little overwhelming and means that without spending some time on the subject, you might not even be aware that some functionalities are available out-of-the-box, so to say. I’ll stick to a few simple ones as examples.

You can create an Observable from an existing click event with fromEvent and then work with the stream of clicks coming from it.

import { fromEvent } from 'rxjs';

const o = fromEvent(document, 'click')
  .subscribe(x => console.log(x))

It doesn’t matter if the stream contains clicks or numbers. You can easily skip the first 4 and then filter out the odd numbers. You’ll have to use a pipe (6) to collect all the operators you want to apply. Range creates an Observable that emits the numbers 0 to 10.

import { range } from 'rxjs';
import { skip, filter } from 'rxjs/operators';

const numbers = range(0, 11)
  .pipe(skip(4), filter(x => x % 2 === 0))
  .subscribe(x => console.log(x));

Tap is a handy little operator that literally lets you do something (in older versions of RxJS it was called ‘do’). In this case, we’re writing the value we see streaming past to the console. This is nice to have while debugging or for logging.

import { range } from 'rxjs';
import { tap } from 'rxjs/operators';

const numbers = range(0, 5)
  .pipe(tap((v) => { console.log('Tap sees: ' + v) }))
  .subscribe(x => console.log(x));

The map operator does pretty much the same as the map you know from VanillaJS arrays; it takes the object given and can change it before passing it on. In this example, we multiply the value by 10.

import { range } from 'rxjs';
import { map } from 'rxjs/operators';

const numbers = range(0, 5)
  .pipe(map((v) => v = v * 10))
  .subscribe(x => console.log(x));

The real fun starts when you see that you can just stick all of them together in the order that you want them to execute, like this:

import { range } from 'rxjs';
import { tap, skip, map } from 'rxjs/operators';

const numbers = range(0, 5)
  .pipe(
    tap((v) => { console.log('Tap sees: ' + v) }),
    skip(2),
    map((v) => v = v * 10)
  )
  .subscribe(x => console.log(x));

Which results in something like this:

That whole Reactive extensions thing sounds overly complicated…

RxJS is not a simple framework to get to grips with. In fact, I hesitate to call it a framework in the context of JavaScript, as frameworks tend to come and go fairly regularly. Rx has been around for a fair while already. Remember it is not only available in JavaScript.

Practice makes perfect as they say. I’ve only just recently started tinkering with it and have barely scratched the surface. That said, RxJS has been implemented in various other frameworks used in the business, so having at least some knowledge of it is very beneficial.

Some examples of other projects that use RxJS internally are (obviously) Angular, ngrx/store, cycle.js, WebRx and others. (7)

As with most frameworks that are popular at the moment, it is in continuous development. Crystal balls do not work, so it is hard to predict what will happen for sure, but it seems very unlikely that RxJS is going to disappear. Functional programming is here to stay for now and so is RxJS.

Last but not least, a word of warning: at the time of writing this, both RxJS 5 and 6 were all over the internet. A simple online search will not always reveal what version is being used in the examples you find. The versions differ a lot, as the libraries have been moved around and many functions were renamed between versions. If it doesn’t seem to work at first, you might be looking at the wrong version.

References

  1. https://gist.github.com/staltz/868e7e9bc2a7b8c1f754
  2. https://github.com/ReactiveX/rxjs/blob/master/doc/observable.md
  3. https://en.wikipedia.org/wiki/Observer_pattern
  4. https://github.com/ReactiveX/rxjs/blob/master/doc/subject.md
  5. https://rxjs-dev.firebaseapp.com/api
  6. https://blog.hackages.io/rxjs-5-5-piping-all-the-things-9d469d1b3f44
  7. https://github.com/ichpuchtli/awesome-rxjs#libraries-built-with-rxjs

Optional reading material

  1. https://www.learnrxjs.io/
  2. https://github.com/ichpuchtli/awesome-rxjs
  3. http://xgrommx.github.io/rx-book/index.html
  4. https://miguelmota.com/blog/getting-started-with-rxjs/

About Martijn

Craft Expert Martijn Kloosterman is part of the .NET team within Craft, the development programme for IT professionals (powered by Centric). If you would like to follow his blog, sign up for the monthly Craft update.

Want to know more about Craft, the development programme for IT professionals? Check out the website.

Tags:.NET

     
Reacties
    Schrijf een reactie
    • Captcha image
    • Verzenden