Oliver Joseph Ash

Full-stack web developer

Reactive programming in JavaScript: modelling values which change over time using Observables

I see a lot of JavaScript that uses let and var for the purpose of modelling values which change over time. In this post I will demonstrate how the same can be achieved with a higher level abstraction called Observables for improved code readability.

Here is a basic counter example that uses let to store the current value of the counter. The variable is re-assigned at various points in the program.

const counterEl =
  document.querySelector('.counter');
const incrementButtonEl =
  document.querySelector('.increment-button');
const decrementButtonEl =
  document.querySelector('.decrement-button');

let counter = 0;

const render = () => {
  counterEl.innerHTML = counter;
};

incrementButtonEl.addEventListener('click', () => {
  counter = counter + 1;
  render();
});
decrementButtonEl.addEventListener('click', () => {
  counter = counter - 1;
  render();
});
render();

Full code: http://jsbin.com/fihilip/1/edit?html,js,output

The flow of the program is like so: on initialisation and when an event happens (i.e. the user clicks the increment or decrement button), we re-assign the global counter variable with the new value and then we call the render function which uses the global counter variable.

What is bad about this approach?

  • Looking at the structure of this code, the flow of the program is not immediately noticeable. For example, it’s not immediately clear when and where the counter variable gets re-assigned, or when and where render gets called. These details are nested in the implementation.

  • We can’t test render because it depends on a variable scoped to this closure.

  • We have to call render in more than one place (once for each event).

  • We are using let to model a value that changes over time. There is nothing about the statement let counter = 0 to tell you that this variable will be re-assigned later, instead you have to infer it from reading the code. In larger modules, this can make code very difficult to read, because any function that depends on this variable may have unexpected results if the variable has been re-assigned unexpectedly. We should instead be striving to write pure functions.

How could we declaratively define a value that changes over time? In functional programming, we have a primitive for representing asynchronous streams of data called Observables (or signals in Elm). The Observable primitive is provided in the RxJS library, and is being considered for addition to the language as part of ES2016.

Our counter can be thought of as a number that changes over time. When the value changes, we want to call render, passing in the new value. Let’s go ahead and declare exactly that.

const render = counter => {
  counterEl.innerHTML = counter;
};

counter.subscribe(render);

For this to work we need to define counter. Conceptually, a counter:

  1. starts with a value of 0;

  2. emits new values by incrementing or decrementing the previous value as the user clicks the respective buttons.

Before we can model counter, we must first model the increment and decrement actions.

Like our counter, these actions can be thought of conceptually as values that change over time. In this case, our actions will emit a new event object each time the user clicks one of the buttons.

const increment =
  Rx.Observable.fromEvent(incrementButtonEl, 'click');

const decrement =
  Rx.Observable.fromEvent(decrementButtonEl, 'click');

Now that we have Observables representing our increment and decrement actions, we can map them to a delta. Increment should add 1, decrement should subtract 1.

const increment =
  Rx.Observable.fromEvent(incrementButtonEl, 'click')
    .map(() => +1);

const decrement =
  Rx.Observable.fromEvent(decrementButtonEl, 'click')
    .map(() => -1);

Then we can define an Observable of deltas by merging our increment and decrement Observables.

const deltas = Rx.Observable.merge(increment, decrement);

We can use this Observable of deltas to update the value of our counter. As we identified earlier, the counter should start with a value of 0, and emit new values by incrementing or decrementing the previous value as the user clicks the respective buttons (i.e. when deltas emits a new value).

scan is like Array.prototype.reduce but instead of reducing an array, we are reducing a stream of values over time: each time the stream emits a new value, we compute the new accumulator (in this case our counter value) from the previous and new values.

const counter = deltas
  .startWith(0)
  .scan((acc, delta) => acc + delta);

There we have it. counter is an Observable that starts with 0 and then scans over values from our stream of deltas to compute the next value. When the counter value changes, we render the new value to the DOM.

How is this better?

  • The code for our counter Observable is strikingly close to our mental model. Try comparing the code to the conceptual definition of a counter value I gave earlier.

  • counter is defined as a const, which means the value of the variable can never be re-assigned, so there are no surprises when this variable is used. This means we can write pure functions.

  • To understand where and when the Observable emits new values, we can refer to its definition and work backwards from there, whereas when we were using let to model our counter, we had to search the codebase for usages of our variable. This is what it means to be declarative.

  • By raising the level of abstraction, the flow of our program is now immediately clear. Whereas before the flow was deeply embedded in implementation details (i.e. nested in the event handler), now the flow is at the very top of our program.

  • This code is also easier to test, because we can pass a counter value into the render function instead of depending on a global variable.

Full code: http://jsbin.com/murupi/1/edit?html,js,output

This way of modelling programs goes by the name of reactive programming. To learn more about this, I recommend reading The introduction to Reactive Programming you’ve been missing by André Staltz. The example I’ve given here demonstrates the basic differences between these two approaches, but the benefits described are even more striking as the program scales.

Thanks to Natalia Baltazar and Sébastien Cevey for reviewing this article.