Refactoring imperative code to RxJS

Refactoring imperative code to RxJS

Published:

Do you struggle to translate imperative code to RxJS? This is a common issue. In this post, we will highlight the reasons why this translation may can be challenging. We‘ll also go through the process in some examples.

Cover image

Imperative vs. reactive code

On a high level, imperative code says how something should be done. Reactive code, on the other hand, says what shall be done. Code describing the “what” is also called declarative code .

This means that in an imperative code style, you are responsible for calling your functions with the correct values at the correct time. If you go the reactive way, you pass callback functions to the library, instead and delegate their call.

So, if we want to migrate code from imperative to reactive style, this is not simply done by refactoring out some functions, but we have to rethink the whole code on a conceptual level.

Example 1: Chat

Let’s assume, we have a simple chat: The user can send and receive messages. Those messages are added to the chat history.

An imperative approach, could look something like this:

const ws = new WebSocket(wsUrl);

ws.addEventListener("message", ({ data }) => {
  appendMessage(JSON.parse(data.toString()));
});

sendBtn.addEventListener("click", () => {
  const msg = readMessage();
  appendMessage(msg);
  ws.send(JSON.stringify(msg));
});

A conversion to RxJS code could look like this:

const ws$ = webSocket<Message>(wsUrl);

const inbound$ = ws$.asObservable();
const outbound$ = fromEvent(sendBtn, "click").pipe(
  map(readMessage),
);

merge(inbound$, outbound$).subscribe(appendMessage);
outbound$.subscribe(ws$);

We notice that appendMessage() is not directly called by us, but instead, we pass the responsibility to call it to RxJS. And it does so, if any new message is emitted by the inbound$ , or the outbound$ observable.

We split up the producing, imperative part — how a message is produced — from the consuming, declarative part — what happens if a message is emitted.

Example 2: Drawing lines

The previous example was simple. Let’s take a look at a more complex example, which may be a bit more challenging: Drawing lines on a canvas with a mouse.

Here is what an imperative solution could look like:

let start: Point;

canvas.addEventListener("mousedown", onMouseDown);
canvas.addEventListener("mouseleave", onMouseLeave);

function onMouseDown(e: MouseEvent) {
  canvas.removeEventListener("mousemove", onMouseMove);
  canvas.removeEventListener("mouseup", onMouseUp);
  start = eventToPoint(e);
  canvas.addEventListener("mousemove", onMouseMove);
}

function onMouseMove() {
  canvas.removeEventListener("mousemove", onMouseMove);
  canvas.addEventListener("mouseup", onMouseUp);
}

function onMouseUp(e: MouseEvent) {
  canvas.removeEventListener("mouseup", onMouseUp);
  const end = eventToPoint(e);
  drawLine(start, end);
}

function onMouseLeave() {
  canvas.removeEventListener("mousemove", onMouseMove);
  canvas.removeEventListener("mouseup", onMouseUp);
}

We have a lot of event listener registrations and removals going on here.

The event listener removals often translate to usages of the switchMap() or take() Operators. They and their variants trigger unsubscriptions to observables. Let’s see what a reactive solution could look like.

const down$ = fromEvent<MouseEvent>(canvas, "mousedown");
const up$ = fromEvent<MouseEvent>(canvas, "mouseup");
const move$ = fromEvent<MouseEvent>(canvas, "mousemove");
const leave$ = fromEvent<MouseEvent>(canvas, "mouseleave");

down$
  .pipe(
    switchMap((downEvent) =>
      move$.pipe(
        take(1),
        switchMap(() => up$),
        takeUntil(leave$),
        map((upEvent) => [
          eventToPoint(downEvent),
          eventToPoint(upEvent),
        ]),
      ),
    ),
  )
  .subscribe(([start, end]) => drawLine(start, end));

As in the previous example, we have the producing code part separated from the consuming part.

The reactive code expresses more the how instead of the what . For example, instead of unregistering the “mousemove” event listener on the first event, we tell RxJS that it should only take one value and complete by simply applying take(1) .

An important detail is that in the first call of switchMap() we pipe the move$ Observable, instead of directly switching to it, like we do with the up$ Observable. This is for two reasons:

First, we want to keep a reference to the downEvent to use it further down the pipeline. If we would simply switch to the move$ Observable, we would lose this reference.

The second reason is that the Operator applications take(1) and takeUntil(leave$) cause a completion and as such an unsubscription from the source Observable. But we wish to be subscribed to down$ all the time, since users shall be able to draw a random number of lines.

Conclusion

In general, it can be quite challenging to refactor imperative code to reactive code. It is a different paradigm and requires a different mindset. Like in the imperative approach, we also need to play around with the code until it fits our needs.

It definitely helps to have some tests in place that verify that your refactoring does not break the behavior.

In the drawing example above, the reactive version took me less time to write than the imperative version. And I started with the reactive version. The more complex the behavior, the higher the payoff of a reactive version.

In some cases, the difference can be very extreme. Imagine, you want to implement an autocompletion search. You can see a reactive solution in my post Should I learn RxJS? . I will not provide an imperative version, to save us some time, but I think you can imagine how complex such a solution would be.

You cannot skip learning at least some Operators, before you can start designing an RxJS pipeline. A good starting point is my article Mastering RxJS Operators .

Never stop learning, and have a nice day.