Skip to main content

Reactive Programming

What is Reactive Programming ?

Reactive programming (or RP for short) is programming with asynchronous data streams.

Actually, this isn't new:

  • Promises are streams of values with a success and error state, and you can "pipe" them with then or catch
  • EventListeners are streams of events
  • Timers (setTimeout/setInterval) are somehow streams of "void/empty" values
  • ReadableStream and Readable are data streams too
  • websockets
  • ... and many more...

So, you've probably already played with asynchronous data streams. Reactive programming is all about this but on steroids. You'll be able to create data streams of anything, not just from click and hover events: variables, user inputs, properties, tasks, data structures,...

On top of that, you are given an amazing toolbox of functions to combine, create and filter any of those streams. That's where the "functional" magic kicks in. A stream can be used as an input to another one. Even multiple streams can be used as inputs to another stream. You can merge two streams. You can filter a stream to get another one that has only those events you are interested in. You can map data values from one stream to another new one. Etc.

A stream is simply a sequence of ongoing values ordered in time.

We capture these emitted values only asynchronously, by defining a function that will execute when a value is emitted. The "listening" to the stream is called subscribing. The functions we are defining are Observers. The stream is the Observable being observed.

Reactive Programming by example

Normally you'll register event listeners like this:

const onClick = () => console.log('clicked');
document.addEventListener('click', onClick);
// later
document.removeEventListener('click', onClick);

With @lirx/core, instead, you will create an Observable:

const onClick$ = fromEventTarget(document, 'click');
const unsubscribe = onClick$(() => console.log('clicked'));
// later
unsubscribe();

The most important part of Reactive Programming is that you have fine control of the flow of your values.

Let's write a piece of code which displays the number of time the user clicked on the document:

In "plain" js:

let count = 0;
let rate = 1000;
let lastClick = Date.now() - rate;
document.addEventListener('click', () => {
if (Date.now() - lastClick >= rate) {
console.log(`clicked ${++count} times`);
lastClick = Date.now();
}
});

With @lirx/core:

const subscribe = pipe$$(fromEventTarget(document, 'click'), [
throttleTime$$$(1000),
scan$$$(count => count + 1, 0),
]);
subscribe(count => console.log(`clicked ${count} times`));

The @lirx/core code is more concise, organized, and clearer. I hope you enjoy the beauty of this approach. And this is just the tip of the iceberg: you can apply the same operations on different kinds of streams, for instance, on a stream of API responses, bytes, timing, etc.; with many other functions available.

For what usage RP is great ?

Usually, when we use variables and functions, we PULL values:

let firstname = 'Valentin';
let lastname = 'Richard';
const fullname = `${firstname} ${lastname}`; // you 'PULL' the values from 'firstname' and 'lastname' to compute 'fullname'
console.log(fullname); // 'Valentin Richard'

But, sometimes (especially in javascript with a lot of async events, or in front-end applications) we have to deal with evolving values: If we update the value of firstname or lastname (ex: from an <input> element), we would like to update the value of fullname too.

We could write:

const getFullName = () => `${firstname} ${lastname}`;
console.log(getFullName()); // 'Valentin Richard'
firstname = `Bob`;
console.log(getFullName()); // 'Bob Richard'

But we're forced to PULL the "fullname" to get the value. It would be more convenient having fullname updating automatically when any of firstname or lastname changes.

Here comes the Signals:

// we assume that `createTextInputSignal` returns a Signal updated with the value of an <input>.
const firstname = createTextInputSignal('Valentin');
const lastname = createTextInputSignal('Richard');
const fullname = computed(() => `${firstname()} ${lastname()}`);

effect(() => {
console.log(fullname());
});

If any of firstname or lastname changes, then fullname is immediately updated. So Signals and Observables PUSHES values. We don't have to PULL them. It's magic, and it's perfect for all async events.

So Reactive Programming is excellent for every asynchronous stream of data. This includes: computed/updated values, events, promises, timers, and many more.

Why should you consider adopting RP?

Reactive Programming raises the level of abstraction of your code, so you can focus on the interdependence of events that define the business logic, rather than having to constantly fiddle with a large amount of implementation details. Code in RP will likely be more concise.

The benefits are really important in web applications, where you have to deal with multitude of UI events, async requests, etc... and react to these changes, like updating the DOM.


If you want to go deeper into RP, I recommend you this excellent article from Andre Staltz.