Abusing Cycle.js for no (good) reason

Disclaimer 1

This blogpost most likely won’t teach you anything useful. It doesn’t focus on a real-life use case; rather a “because I can” type of situation. If you’re a pragmatic developer, you should probably focus on studying AngularJS 3.0 or learning to parse HTML with regular expressions. I’d also consider looking into the latest major jQuery release, if I were you. Nothing will make you leak memory with more style. However, if you somehow managed to get through this paragraph, keep on reading.

Disclaimer 2

It’s 2016. I’m not even gonna mention that I’m using build tool X to bundle modules & transpile stuff. I’m not gonna explain destructuring assignment, arrow functions, default parameters, and TDZ. All those have been with us for a while now and simply assuming that the reader is familiar with them doesn’t seem like a bad idea at all.

Disclaimer 3

I’m relatively new to the topic I’m about to rant on, therefore chances are that I’m gonna drift away from what’s commonly known as best coding practices. Still, bear with me.

Lorem ipsum

Among all the JavaScript awesomeness, there’s Cycle.js. In theory, it’s a framework. In practice, a very unusual one. One thing definitely makes it stand out from other frameworks out there – its size. Not only is it more lightweight than Ext JS, it’s actually not much heavier than ParisHilton.js. For those who feed with source code, here’s Cycle.js in all its glory. It’s currently ~100 lines of code, but the fun fact is that it used to be heavier, but over time shrunk by about a half. Okay, so what makes it so tiny, yet still capable? Three things in particular:

  • Cycle.js is built on top of another awesome tool – RxJS.
  • The only thing Cycle.js actually does is manage a unidirectional, cycle-like data flow.
  • All side effects (both: read & write) are managed by pieces of code external to the framework — so called drivers.

Let’s dig a bit into each of these:

Built on top of RxJS

RxJS is a state-of-the-art piece of code indeed. Among other useful stuff, it provides a super versatile data structure – the Observable. Observables may be considered as collections of values spread over time. We can transform them in the same manner we would with synchronous collections, using operators. Operators are methods (either static, or instance-level) letting us perform certain transformations (mapping, filtering, combining) on observables. All operators return new observable, the result of applying a transformation to the source observable, which can be then transformed further. Too see the power of observables, take a look at the following example:

import { Observable } from 'rx';

Observable.fromEvent(document.body, 'click')
    .filter(e => {
      const { clientX: x, clientY: y } = e;

      return x <= 100 && y <= 100;
    })
    .withLatestFrom(
      Observable.interval(1000),
      (ev, tick) => ({ ev, tick })
    )
    .filter(({ tick }) => tick % 2)
    .subscribe(({ ev }) => console.log(ev));

In the code above, we log the events of clicking the <body> to the console. To make things more complicated, we only log the ones that happened in the top left corner of the screen (a 100x100px rectangle), within the odd ticks of our timer. How convoluted would the code be if not for observables? Since all this power comes straight from RxJS, Cycle.js is pretty capable out of the box.

Unidirectional data flow

Since at its heart every Cycle.js app is a (pure) function, it pretty much works like one. Every function has an input and produces an output. In the world of Cycle.js, the input is called sources, the output are sinks. What happens between returning sinks and feeding sources back to Cycle.js app are side effects managed by drivers, which we’ll get to shortly. For now, let’s focus on how sources are turned into sinks.

import { run } from '@cycle/core';

function main(sources) {
  return sinks;
}

const drivers = {};

run(main, drivers);

The whole application logic resides within the main function. The main function takes sources as a parameter and returns on object of sinks which is then consumed by the run function. Within the run function, drivers are fed with proper sinks to produce sources. The sources are then passed back to the main function and the ‘cycle’ is complete. However, there’s some trickiness to it which Cycle.js aims at resolving. Take a look at the following example:

import { run } from '@cycle/core';
import { div, makeDOMDriver } from '@cycle/dom';

function main({ DOM }) {
  const vtree$ = DOM
      .select('div')
      .events('click')
      .startWith(0)
      .scan(counter => counter + 1)
      .map(counter => div(`Clicks: ${counter}`));

  return {
    DOM: vtree$,
  };
}

const drivers = {
  DOM: makeDOMDriver(document.querySelector('#app')),
};

run(main, drivers);

The code above renders a <div> with a number of clicks inside. Every time we click it, the number gets updated with the current value. Let’s examine the lifecycle of the app:

  • The main function is called. It returns an object containing one sink (DOM):
const sinks = main(sources);
  • The sources are created by calling driver functions with appropriate sinks:
const sources = {

  DOM: drivers.DOM(sinks.DOM),

};

The thing is that at the time of creating sinks, sources is still undefined. This is where Cycle.js kicks in. For creating sources, instead of using the original sinks, it loops over them and creates a ‘proxy sink’ for every single one. It then subscribes the original sinks to their proxies, hereby ‘closing’ the cycle. The data in the app flows from the sources, through the logic within the main function, then to the drivers through sinks, and comes back as sources again. That’s pretty much all the magic.

Side effects = drivers

While the core logic resides within the main function, all the side effects belong to drivers. By side effects I mean all the interaction with the external world. Side effects can be either read, write or both; so can drivers. Think of drivers as a way to do AJAX, WebSockets, interact with the dom, talk to a remote backend. All kinds of stuff. Let’s get back to the example above. The code is only using one driver – the DOM driver, which it creates by calling:

makeDOMDriver(document.querySelector('#app'))

The first argument here is a reference to the root element to render the app in. The DOM driver is both: readable & writable. We create the actual DOM by returning a sink called DOM, containing a stream (observable) of virtual DOM (the DOM driver uses virtual-dom under the hood:

return {
  DOM: vtree$,
};

Reading from the driver (from the DOM, actually) is done by calling the select() method of the DOM source and passing it a valid CSS selector. We can then narrow the focus by calling the events() method of the object returned from the previous call and passing the name of event we’re interested in. We get back a stream (observable) of events on element(s) matching the selector:

The stream can be then used to alter the current state and construct new virtual-dom stream; and so on.

This is how you’d use a driver, but what’s under the hood? Unsurprisingly, drivers are simply functions. The makeDOMDriver from above is just a higher-order function that returns the actual driver function. The arguments passed to makeDOMDriver are then available from within the driver via a closure.

The anatomy of a driver

Here’s a skeleton of a typical Cycle.js driver:

function driver(optionalSink) {
  …
  return optionalSource;
}

As drivers can decide whether they want to read or write data, neither passing in sinks nor returning sources is mandatory. Examine the example below. It’s a write-only driver that only alerts the text passed to it. It is of course the stupidest thing to do since alerts are blocking, still due to its simplicity, alert makes for a great example:

function alertDriver(txt$) {
  return txt$
      .subscribe(txt => alert(txt));
}

Taking this idea further, let’s create a read-write driver using prompt in place of alert. The driver’s gonna read from the prompt dialog (with provided label) and write the data back to the prompt source. Again, this implementation is super-naïve but will do just fine for learning purposes:

function promptDriver(label$) {
  const promptProxy$ = new Subject();

  label$.subscribe(label => promptProxy$.onNext(prompt(label)));

  return promptProxy$;
}

Let’s end this section with an (useless, as usual) example of combining both:

import { Observable, ReplaySubject } from 'rx';
import { run } from '@cycle/core';
import alertDriver from './alertDriver';
import promptDriver from './promptDriver';

function main({ prompt }) {
  const label$ = Observable.just('What's your name?');
  const txt$ = new ReplaySubject();

  prompt
      .map(name => `Hello ${name}!`)
      .subscribe(txt$);

  return {
    prompt: label$,
    alert: txt$,
  };
}

const drivers = {
  alert: alertDriver,
  prompt: promptDriver,
};

run(main, drivers);

To the point

Finally, we’re in the point where (hopefully) the basic mechanics of Cycle.js and the concept of drivers are a bit more familiar. Now, let’s get to the ‘Abusing Cycle.js’ part. I recently watched a bunch of videos from CycleConf 2016 (which I highly recommend; especially this particular one). One of them kinda opened my eyes. The talk I mean is the one by Hadrien de Cuzey. I’m not sure what the original title was but the first slide stated “Why? Why not?”. The topic of the talk was creating shell applications with Cycle.js. It’s probably just me, but when I think of JavaScript applications, I automatically think of rendering to the DOM. For the same reason, when I think of Cycle.js applications, I think of the DOM driver. However, my plan for the past couple of days was to switch from the ‘because I should’ mindset to a ‘because I can’ one. I asked myself a question: what’s the stupidest Cycle.js driver I can possibly create? Alternatively, Is there something no one has done before that I can create and distribute as a Cycle.js driver? And indeed, I managed to come up with something extremely unusual. How about rendering to localStorage? Even better – how about animating it?

Even though I was unable to come up with any potential use case (apart from posting job offers for developers maybe…), I came up with specs for this brilliant piece of software engineering:

  • The driver should take a stream of strings to animate.
  • The driver should return an stream of animation frames.
  • Once pushed, the string should animate until another one is pushed.
  • The animation should resemble the one known from the infamous marquee element.
  • Things like size, speed, placeholder char & localStorage key should be customizable.

The driver is now available on npm and can be installed via:

npm install --save cyclejs-animated-localstorage

Here’s the project on GitHub:

https://github.com/rkrupinski/cyclejs-animated-localstorage

Demo time

The simplest way of using the driver would be:

import { Observable } from 'rx';
import { run } from '@cycle/core';
import makeAnimatedLocalStorageDriver from 'cyclejs-animated-localstorage';

function main() {
  const text$ = Observable.just('¯\_(ツ)_/¯');

  return {
    animatedText: text$,
  };
}

const drivers = {
  animatedText: makeAnimatedLocalStorageDriver({
    key: 'demo',
    size: 25,
  }),
};

run(main, drivers);

demo1

Alternatively, with repeating text:

import { Observable } from 'rx';
import { run } from '@cycle/core';
import makeAnimatedLocalStorageDriver from 'cyclejs-animated-localstorage';

function main({ animatedText }) {
  const text$ = Observable.zip(
    Observable
        .fromArray(['RULES', 'TIVIX']).repeat(10),
    animatedText
        .map(() => 1)
        .scan((acc, curr) => acc + curr, 0)
        .filter(count => !(count % 30)),
    text => text
  )
      .startWith('TIVIX');

  return {
    animatedText: text$,
  };
}

const drivers = {
  animatedText: makeAnimatedLocalStorageDriver({
    key: 'demo',
    size: 25,
  }),
};

run(main, drivers);

demo2

Abusing Cycle.js for no (good) reason

Leave a Reply

Your email address will not be published. Required fields are marked *