Alex BespoyasovAuthor's photo

Click Me! Written with RxJS

Last weekend I needed to mess around with RxJS, so I tried to write a game with a button that runs away from the cursor. In this post I'll show what I used to write the app, and go through the process step by step.

If you've worked with this technology before, you probably won't learn anything new. This post is aimed at people like me, who from RxJS know only JS 😃

The game will be written in TypeScript. There won't be much difference from regular JS, but it's still worth looking at its documentation to know how to declare types of variables and return values from functions.

Why TypeScript

I've been wanting to try it for a long time, and RxJS is written in it. I thought, why not add restrictions and headaches to myself, so here we are ¯\_(ツ)_/¯

What's RxJS

RxJS is an implementation of ReactiveX for JS. ReactiveX in turn, according to their site, is an API for asynchronous work with observable streams. It took me a while to understand it, so let's look into it one by one.

Primitively speaking, a stream is a sequence of something: events, data, transformations, etc. Imagine a chat room where you're chatting with someone. The sequence of messages in it is a stream.

A stream is observable if we can subscribe to it—declare a function that will handle each new item. A stream is an observable stream if we sit and read each new message.

The benefit of streams is that they make it possible not to handle, for example, events one by one, but to combine them and work with a set of events at once.

Observer and Observable

ReactiveX fundamentally uses the “Observer” pattern. The two main concepts we will need are observer and observable.

The observable would be a stream: it sends elements from some source one at a time. This can be thought of as a river on which the “element ships” are floating.

The observer is the object that knows what to do with the elements from the stream and how to handle them. This can be thought of as a child who wants to collect the ships and take them home.

The observer is connected to the observable through a subscription, a function that passes elements from the observable to the observer. It is like a net that catches ships in the river. When a new ship enters the net, the child notices it and can pick it up.

The observable stream knows how to notify about:

  • a new item;
  • an occurred error;
  • items have run out.

To all of this the observer can respond in some way.

Diagrams

To understand the concept of streams better, the RxJS documentation offers so-called marble diagrams. They depict balls, which seem to be strung on a rope.

Example diagram from documentation
Example diagram from documentation

These balls are the elements in the stream. A stream is a line of time pointing from left to right. If an item is to the left, that means it appeared earlier.

(It would of course be cooler to show all this by animation: you know, like elements falling down one by one, going through a transformation, falling further down.)

Describe the Game

To write a game, we have to define what events we are going to handle and what we want to do with them.

We will monitor mouse movement and check where the cursor is. If it is within 15 pixels of the button, we will redraw the button.

Game Diagram
Game Diagram

A little closer to the code, we will have a stream of mouse movement events. We'll clean them up and leave only the coordinates {x, y}. Then we'll filter the coordinates by checking if the cursor is close enough to the button:

Concept game diagram a little closer to implementation
Concept game diagram a little closer to implementation

Let's Code

In RxJS you can make a stream from anything: from an array, a promise, events in a browser. For example, you can make it from an array using the from operator:

import { from } from "rxjs";

// Extracts one element at a time out of the array until they run out.
const arraySource = from([1, 2, 3, 4, 5]);

The source for our stream will be the mouse movement event on the screen. To create a source from a browser event, we will use fromEvent:

import { fromEvent } from "rxjs/observable/fromEvent";

const source = fromEvent(document, "mousemove");

Now the browser event mousemove will be tracked within document, and for each move a new element will appear in source.

We will transform and filter these elements. After each transformation we will get a new observable with elements that we can interact with again somehow.

Operators

Operators are functions that can transform elements after observable has sent them.

To apply several transformations one by one, we need pipe. This is a method that deals with the composition of operators, that is, it applies them in order.

import {map, filter} from 'rxjs/operators'

// ...

const observable = source.pipe(
  map(...),
  filter(...)
)

The map operator is needed to apply some function to each element.

We want to extract from each event the coordinates of the mouse on the screen. So in map we will pass a function that will retrieve this data and return the object.

map((event: MouseEvent): MouseCoords => ({ x: event.x, y: event.y }));

MouseCoords is the data type we will create to handle coordinates. It will be an object with fields {x, y}. It's not necessary to create a new type, but it's easier with the type to understand what we're working with.

type MouseCoords = {
  x: number;
  y: number;
};

// ...

map((event: MouseEvent): MouseCoords => ({ x: event.x, y: event.y }));

The filter operator will select events that fit the condition we need.

The event suits us if the cursor is within 15 pixels of the button on both axes.

const shouldUpdateApp = ({ x, y }: MouseCoords): boolean => {
  const { top, left, widthRange, heightRange } = state.get();
  const padding = 15;

  return (
    inRange(x, left - padding, widthRange + padding) &&
    inRange(y, top - padding, heightRange + padding)
  );
};

// ...

filter(shouldUpdateApp);

And then the observable code will look like this:

const source = fromEvent(document, "mousemove");

const observable = source.pipe(
  map((event: MouseEvent): MouseCoords => ({ x: event.x, y: event.y })),
  filter(shouldUpdateApp),
);

Event Subscription

Every element in the stream tends to get to subscribe, where it will be handled somehow.

The subscribe method takes three argument functions. The first function handles new elements, the second handles an error if one occurs, and the third handles the end of the stream:

observable.subscribe(
  // `onNext` is called when new elements appear, `el` is the new element.
  (el) => {},

  // `onError` is called if an error occurs, `er` is the error object.
  (er) => {},

  // `onCompleted` is called when the stream is finished.
  () => {},
);

When a new item appears, we will call the updateApp function, which will generate random coordinates for the button, update the state of the application and redraw the button:

observable.subscribe(() => updateApp());

const updateApp = () => {
  const { left, top } = getNewPosition();
  state.update({ left, top });

  applyStyle(button, {
    left: `${left}px`,
    top: `${top}px`,
  });
};

Results

I will not elaborate on the class that controls the state of the application and the helper functions. You can look at the source code of the whole thing on GitHub.

The game itself is very simple, although it's ok to get acquainted with RxJS.

Of course, there's a lot more stuff I haven't told you about: creation of threads from promises, Subject, Scheduler, lots of operators, which sometimes you can't figure out without special service. But it will do for starters.

Resources

Observer Pattern and Functional Reactive Programming

RxJS Documentation

Operators

Previous post: How I Divide My Work TimeNext post: Rules of Work Communication by M. Ilyahov and L. Sarycheva