Learning Cycle.js

Written by Liam McLennan

Cycle.js logo

Cycle.js is clearly React inspired, but it attempts to fix some areas where React is deficient, or weird. It also advocates (forces?) a functional style, composing immutable state via observables.

Even with a React background the learning curve is steep. I started to see how it is possible to reason about Cycle.js applications when I saw the refactoring to the Intent-Model-View pattern. See jsbin.

This post includes graphics and code samples from the Cycle.js website.

Intent

Intent is typically a function from sources of input (drivers) (DOM, HTTP, etc) to a set of observables, named according to the user’s intent (e.g. changeHeight$, receiveUserDetails$).

function intent(DOM) {
  return {
    changeWeight$: DOM.get('#weight', 'input')
      .map(ev => ev.target.value),
    changeHeight$: DOM.get('#height', 'input')
      .map(ev => ev.target.value)
  };
}

Model

Model is a function from intents (produced by the Intent function) to a state$ observable. The latest value of the state$ observable is the current state of the component. state$ is observed by the view, so changes to state$ cause changes to the view. Earlier examples had the state spread across multiple observables (e.g. one for the DOM, one for each possible HTTP request). Consolidating state into a single observable takes some work but makes the component easier to reason about and the view easier to implement.

function model(actions) {
  return Cycle.Rx.Observable.combineLatest(
    actions.changeWeight$.startWith(70),
    actions.changeHeight$.startWith(170),
    (weight, height) =>
      ({weight, height, bmi: calculateBMI(weight, height)})
  );
}

View

View is a function from the model observable (state$) to a vdom observable. This is equivalent to React’s Render function.

function view(state$) {
  return state$.map(({weight, height, bmi}) =>
    h('div', [
      renderWeightSlider(weight),
      renderHeightSlider(height),
      h('h2', 'BMI is ' + bmi)
    ])
  );
}

main()

Group Created with Sketch. Human Computer

A cycle.js application includes a function that represents the computer side of the human-computer interaction, often called main. It is a function from input drivers (http responses, DOM events) to output drivers (http requests, vdom structure). The Get Random User example shows working with two drivers (HTTP and DOM).

function main({DOM}) {
  return {DOM: view(model(intent(DOM)))};
}

Cycle.run()

An application is started by calling Cycle.run() specifying the main function and the required drivers. E.g.

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