The Inner Workings of Cycle.js

The core of Cycle.js is incredibly small but highly efficient to meet the demand for modeling flow of data and dealing with the communication between the application and the outside world. With Cycle.js, your program is a closed world, but obviously, if nothing comes in or out of the program, it is useless. Cycle.js provides a simple and ingenious way to compose your program’s interaction with the outside world while maintaining a true separation of concerns. A Cycle.js program’s entry point is called main(). In this function, the program is enclosed. Everything the program does happens from within that function.

As noted above, if nothing can come in or out of the program, it is useless. The input to the program is in Cycle.js known as sources.

One of the unique points of Cycle.js is how the sources are injected to the program. Cycle.js comes with an on-switch simply called run(). This is one of the two APIs that exist in Cycle.js and is used to kick-start Cycle.js. run() takes two arguments: a function and an object. The function is the program’s entry point main. The object is the sources that main should know about.

As you can see, we still haven’t declared what the sources are. The sources in Cycle.js are provided as drivers. A driver is the connection point to the outside world. Drivers are where side effects take place, e.g., updating the DOM in the browser, or sending a message to a server, but they also provide input to the program and reads output from the program. Cycle.js comes with a couple of common drivers, for example, the DOM driver. The DOM driver uses virtual-dom under the hood to effectively handle DOM manipulation and DOM state. Drivers are usually made with a factory function which accepts some options. The factory function will then return the actual driver function configured with the options. In case of the DOM driver factory function, it takes a CSS selector as argument.

Now our program can access the DOM driver’s API for reading events that happen in the DOM. As you recall, everything is a stream of events, and because Cycle.js is an extension of RxJS, drivers provide these streams of events from the outside world.

As the avid reader will recall, Cycle.js is essentially about modeling flow of data. This is the early beginning to start visualizing this flow. We now have a flow of input event data from an HTML element streaming into our program, available for it to use and act upon. But wait a minute. How can we then listen for events on a nonexistent element? We haven’t told the DOM to render any HTML INPUT element. That is true, and we won’t. Telling is imperative, and imperative commands don’t belong in our closed-world program. Even if we did choose to make imperative statements, the DOM is the outside world, and it is the responsibility of the driver to handle the rendering. Thus, imperative commands should exist solely in drivers.

The way the driver receives information about what it should do is by the same means of a push-based system. The program pushes information to the driver in the same form of event streams that the driver pushes to the program. Cycle.js uses the term sinks for the streams that are pushed out of the program to the outside world, i.e., the drivers. We simply return the sinks from the main().

In Cycle.js, the sinks are, like the sources, an object. The sinks’ keys correspond with the driver names, i.e., the keys in sources, to enable Cycle.js to do the mapping.

This code will actually execute and display an INPUT range slider in the browser. However, it doesn’t do anything else. The alert reader will probably wonder how we can listen for input events on the INPUT element when it is declared later in the code. The answer lies hidden in the core of Cycle.js, and the following explanation should complete the understanding of the flow of data in Cycle.js.

When run(main, sources), the kick-starter, is called, Cycle.js creates a proxy sink for each driver, and then calls each driver passing it the proxy sink. The proxy sink is both a replay-able, observable stream and an observer in one, known in Rx as a ReplaySubject. It is configured to replay just the last element. The results of each driver invocation are stored in a sources object and then passed to main(sources). main proceeds to execute, now having access to the driver APIs. When main returns the sinks, Cycle.js creates a disposable subscription to which each sink, i.e., the returned observable streams from main, is added and subscribed to, using the proxy sink as observer.

In effect, Cycle.js circularly connects main with the drivers, creating a feedback loop of data. The driver source observables are streaming events (or pushing data) into main, and main’s sink observables are streaming events (or pushing data) out to the drivers, which then can produce side effects based on the data from the program.

We will finish the example in the Nested Dialogues and Model, View, Intent section. However, before you jump to that, it is advisable to read the next sections first to get a better grasp of how Cycle.js and its constituents work.

An Extension of RxJS

Cycle.js embraces functional reactive programming (fRP). Because programming is about transformation of data, and architecture is about flow of data, Cycle.js is really nothing more than functions (transformation) and observables (data flow). The observable streams as input to functions are called sources, and the observable streams as output from functions are called sinks. The observable streams that make the data flow are at the heart of Cycle.js; thus, Cycle.js is really just a clever extension of RxJS. Cycle.js, in its true essence, simply connects the data flow in a unidirectional cycle, a feedback loop, using drivers that are functions and can produce side effects and main() which is the actual program function.

Unlike frameworks like React and Angular, Cycle.js’s use of RxJS is not a last-minute patch. It is build around RxJS (though ports to other reactive libraries are quite possible). This is an enormous advantage and gives rise to the explicit data-flow graphs, which is unachievable with React and Angular.

Drivers For Side Effects

As described previously, drivers are the building blocks for handling side effects. Any effect that has interaction with calling functions or the outside world, i.e., outside main(), is considered a side effect, e.g., drawing a pixel on the screen, storing a value in a database, or sending a message to a server. When our program requires side effects, we use a driver. Cycle.js offers, at current time of writing, the following drivers:

Understanding the Driver

We will build a very simple driver that doesn’t really do much, but it will be easier to reason about the basic structure of drivers and fitting the terminology that were introduced earlier.

Using the above factory will only give us a no-op function, which is pretty useless, so let’s do something about that. Remember that a driver, just as our program, accepts data input and exits data output. The terminology is source and sink. The driver accepts a sink from main(), but from the drivers perspective, it is a source of data. Likewise, the output from the driver’s perspective is a sink, but a source from the program’s perspective.

It should be noted that it is not a requirement that the driver returns an observable stream as output. It could, as in the case of the DOM driver, be an object with methods, or nothing at all for that matter. Likewise, it is also not a requirement that the driver accepts a source. When a driver accepts a source as input, it is a sink driver. Yes, this sounds strange and can easily cause confusion, but remember that from the program’s perspective, we return a sink that the driver accepts. When a driver returns a sink as output, it is a source driver because the program gets it as source. Our driver is a source-and-sink driver. As a rule of thumb, if the driver provides data to the program, it is a source driver, and if the program provides data to the driver, it is a sink driver.

If we don’t require our driver to be configured with options, we can remove the outer factory function altogether. The factory function in above example is there to illustrate the common way of structuring drivers.

Here is how we can use it in our Cycle.js program: