Working with HTTP streams with Cycle.js

in JavaScript

Hi! These days everyone is talking about functional programming in JS, reactive programming, and streams. Some awesome libraries came out to help us to work with streams and my favorite is Cycle.js. It's created by André Staltz.

I've recently started to use Cycle.js framework for reactive programming. This framework gives you the ability to think about what should your app do when some data comes in the stream (Sources) and what it should do when you send some response back (Sinks). It creates an awesome circle between Sources and Sinks and that's why it's called Cycle!

Let's quote text from cycle.js.org website. It perfectly explains what's the main function of Cycle.js framework:

Cycle’s core abstraction is your application as a pure function main() where inputs are read effects (sources) from the external world and outputs (sinks) are write effects to affect the external world. These side effects in the external world are managed by drivers: plugins that handle DOM effects, HTTP effects, etc.

It basically tells everything, Cycle.js creates a cycle between your inputs and outputs and gives you ability to write code that's in the middle.

Building app

TL;DR I've already created app, it's on github: https://github.com/ivanjov/cycle-js-http-tutorial. There is also simple explanation how to test it on local environment.

Setting up

First of all, we will need to install Cycle.js and XStream, with their components:

npm install @cycle/dom @cycle/http @cycle/xstream-run xstream --save

This is enough for now. We will install and create Express server later (we will use it for getting random JSON data) and test our Cycle.js app.

Main.js file

In our main.js file, we are going to import Cycle.js and other important components:

import Cycle from '@cycle/xstream-run';
import xs from 'xstream';
import {div, h2, makeDOMDriver} from '@cycle/dom';
import {makeHTTPDriver} from '@cycle/http';

We will also call Cycle.run function and inject out main function:

function main(sources) {}

Cycle.run(main, {
  DOM: makeDOMDriver('#main'),
  HTTP: makeHTTPDriver()
});

Cycle.run function takes main function as the first argument (that function will make magic) and passes sources as the second argument:

  • DOM: That's our DOM driver that connects to div with id main. Cycle.js is awesome because we can select just one div and implement whole logic inside it, as we do with React for example.
  • HTTP: This is our HTTP driver that take our URL stream, make request to those URLs and pass responses to main function

Main logic

Here we are! We have our main function and now's the time to create some magic!

We will first create our request stream:

const request$ = xs.periodic(1000)
    .mapTo({
      url: 'http://localhost:3000',
      category: 'api',
    });

I think that this is very clear. We are creating streams of URLs and that will later be wired to HTTP driver. HTTP driver then makes GET requests to http://localhost:3000 every 1000 milliseconds. We are then mapping it to category api. It's important to set a category for every request we create from Cycle.js because we can listen for specific request later.

Now we have our request stream and we need stream listener that will do something on every response from HTTP driver. We are changing our DOM on every request. Here is the code for that:

const vtree$ = sources.HTTP.select('api')
    .flatten()
    .map(res => res.body)
    .startWith({
      number: 0
    })
    .map(result =>
      div([
        h2('.label', `Random number from server: ${result.number}`)
      ])
    );

It's taking HTTP responses from stream, selects our api stream (as we talked about later, we can select a specific category to listen to). It's then mapping response, selecting just body of the response and pushing result to the DOM.

Cycle.js also gives us ability to set a default value, so we set it to 0:

{
  number: 0
}

This is also the response format that we want to get.

We also need to flatten our stream: https://github.com/staltz/xstream#flatten (check out the diagram, it explains everything). Our source stream will return multiple streams and we can only use one at the time.

Also, Cycle.js DOM is based on Virtual DOM library called snabbdom and it's awesome library for rendering DOM elements. You will get used to it in couple minutes. Cycle.js can also use JSX (scroll down to find documentation how to use JSX with Cycle.js).

Final main.js file should look like this:

import Cycle from '@cycle/xstream-run';
import xs from 'xstream';
import {div, h2, makeDOMDriver} from '@cycle/dom';
import {makeHTTPDriver} from '@cycle/http';

function main(sources) {
  const request$ = xs.periodic(1000)
    .mapTo({
      url: 'http://localhost:3000',
      category: 'api',
    });

  const vtree$ = sources.HTTP.select('api')
    .flatten()
    .map(res => res.body)
    .startWith({
      number: 0
    })
    .map(result =>
      div([
        h2('.label', `Random number from server: ${result.number}`)
      ])
    );

  return {
    DOM: vtree$,
    HTTP: request$
  };
}

Cycle.run(main, {
  DOM: makeDOMDriver('#main'),
  HTTP: makeHTTPDriver()
});

HTML file

We need to include Cycle.js code and our main.js in HTML file that will server everything. File should look like this:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Cycle.js HTTP request example</title>
</head>
<body>
    <div id="main"></div>
    <script src="./dist/main.js"></script>
</body>
</html>

main.js will be in dist folder after we connect modules using browserify and transpile code using babel.

Creating server

As you saw, we are taking responses from server listening on http://localhost:3000. That is simple Express server that returns random number in JSON format.

We need to install express and cors library (for allowing access from different domains):

npm install express cors --save

Our server.js should look like this:

import express from 'express';
import cors from 'cors';

let app = express();
app.use(cors());
app.options('*', cors());

app.get('/', (req, res, next) => {
  return res.json({number: getRandomInt(1, 1000)});
});

app.listen(3000, () => console.log('Server listening on port 3000'));

const getRandomInt = function(min, max) {
  min = Math.ceil(min);
  max = Math.floor(max);
  return Math.floor(Math.random() * (max - min)) + min;
};

Running the app

The app we created now is on (github)[https://github.com/IvanJov/cycle-js-http-tutorial]. I highly recommend to clone it and run in your local environment. I've setup all scripts in package.json and you should just call

npm run start

and to open index.html file in your browser.

You can also play with the code from this page, but you will need to setup browserify and babel.

When you open index.html, you should see something like this:

Conclusion

Cycle.js is an awesome library for reactive programming. It gives you enough space to experiment with your ideas but also gives you enough support and makes sure that everything will go well. It's perfect for real-time apps, with a lot of client-server conversation. You can create awesome apps with it, just give it a chance!

Comments