JavaScript - - By Mark Brown

Fun Functional Programming with the Choo Framework

This article was peer reviewed by Vildan Softic and Yoshua Wuyts. Thanks to all of SitePoint’s peer reviewers for making SitePoint content the best it can be!

Train going over a bridge of iDevices

Today we’ll be exploring Choo by @yoshuawuyts — the little framework that could.

It’s a brand new framework to help you build single page apps that includes state management, unidirectional data flow, views and a router. With Choo you’ll be writing similar style applications to React and Redux but at a fraction of the cost (file size) and number of API’s. If you prefer minimal frameworks and like playing with new technology at the bleeding edge, you’ll enjoy exploring Choo. Because it’s so slender another place it makes a lot of sense is for mobile web apps where you should keep the file size to a minimum.

There’s nothing genuinely new that Choo introduces, it simply builds on top of a lot of good ideas that have come from React, Redux, Elm, the Functional Programming paradigm and other inspirations. It’s a neat little API that wraps all of these good things into one cohesive package you can install and start building single page apps.

This article will be covering Choo v3. At the time of writing v4 is in alpha so you’ll need keep an eye out for changes — this train is moving quickly.

Note: This article will make most sense if you have some knowledge of a declarative view library like React and a state management library like Redux. If you don’t have experience with those yet you might find Choo Docs – Concepts offer more in depth explanations of the important concepts.

Do Try This At Home

Follow along by pulling down the demo repo and installing the dependencies.

git clone https://github.com/sitepoint-editors/choo-demo
cd choo-demo
npm install

There’s npm scripts to run each of the examples e.g.

npm run example-1
npm run example-2

Hello Choo

First, we need to require the choo package and create an app.

View file on GitHub: 1-hello-choo.js

const choo = require('choo')
const app = choo()

We use models to house our state and functions to modify it (reducers, effects & subscriptions), here we initialize our state with a title property.

app.model({
  state: {
    title: '🚂 Choo!'
  },
  reducers: {}
})

Views are functions that take state as input and return a single DOM node. The html function that ships with Choo is a wrapper around the yo-yo package.

const html = require('choo/html')
const myView = (state, prev, send) => html`
  <div>
    <h1>Hello ${state.title}</h1>
    <p>It's a pleasure to meet you.</p>
  </div>
`

This html`example` syntax may be new to you but there’s no magic going on here, it’s an ES6 tagged template literal. See the Let’s Write Code with Kyle episode for an excellent explanation of them in detail.

Routes map URLs to views, in this case / matches all URLs.

app.router(route => [
  route('/', myView)
])

To get this locomotive moving we call app.start and append the root node to the document.

const tree = app.start()
document.body.appendChild(tree)

And we’re done. Run npm run example-1 and you should see the following document:

<div>
  <h1>Hello 🚂 Choo!</h1>
  <p>It's a pleasure to meet you.</p>
</div>

We’re making solid progress through Choo’s tiny API. We have basic routing in place and are rendering views with data from our models. There’s not all that much more to learn really.

Read more in the docs: Models, Views

Running Choo in the Browser

If you’re following along at home the examples are all using a dev server named budo to compile the source with browserify and run the script in a simple HTML page. This is simplest way to play with Choo examples but you can also easily integrate Choo with other bundlers or take a look at the minimal vanilla approach if that’s your jam.

Ch-ch-ch-changes

Now I’m sure by this point your mind is blown, alas there is zero point of using Choo to render static content like this. Choo becomes useful when you have changing state over time and dynamic views: that means responding to events, timers, network requests etc.

Events in the view can be registered with attribute such as onclick, see the complete list of yo-yo’s event attributes. Events can trigger actions with the send function passing in the name of a reducer and data.

View file on GitHub: 2-state-changes.js

const myView = (state, prev, send) => {
  function onInput(event) {
    send('updateTitle', event.target.value)
  }

  return html`
    <div>
      <h1>Hello ${state.title}</h1>
      <p>It's a pleasure to meet you.</p>
      <label>May I ask your name?</label>
      <input value=${state.title} oninput=${onInput}>
    </div>
  `
}

Reducers will look familiar if you’ve used the popular Redux library, they’re functions that take the previous state and a payload and return a new state.

app.model({
  state: {
    title: '🚂 Choo!'
  },
  reducers: {
    updateTitle: (data, state) => {
      return { title: data }
    }
  }
})

View updates are handled by morphdom. Like with React you don’t need to worry about manual DOM manipulation, the library handles transforming the DOM between state changes.

Run the example: npm run example-2

A Component Tree

It makes sense to break up a complex UI into small manageable chunks of UI.

Views can include other views passing down the data they need as well as the send function so that the child components can trigger actions.

Our new view will take an item as input and output an <li> which can trigger the same updateTitle action we saw previously.

View file on GitHub: 3-component-tree.js

const itemView = (item, send) => html`
  <li>
    <span>Go ahead ${item.name},</span>
    <button onclick=${() => send('updateTitle', item.name)}>make my day</button>
  </li>
`

Views are just functions so you can call them in any expression within a template literal placeholder ${}.

const myView = (state, prev, send) => html`
  <div>
    <ul>
      ${state.items.map(item => itemView(item, send))}
    </ul>
  </div>
`

There you have it, Choo Views inside Choo Views.

Run the example: npm run example-3

Effects

Effects are functions that can fire off other actions and don’t modify the state directly. They are the same as action creators in Redux and can handle asynchronous flows.

Examples of effects include: performing XHR requests (server requests), calling multiple reducers, persisting state to localstorage.

View file on GitHub: 4-effects.js

const http = require('choo/http')
app.model({
  state: {
    items: []
  },
  effects: {
    fetchItems: (data, state, send, done) => {
      send('updateItems', [], done)
      fetch('/api/items.json')
        .then(resp => resp.json())
        .then(body => send('updateItems', body.items, done))

    }
  },
  reducers: {
    updateItems: (items, state) => ({ items: items })
  }
})

Effects can be called with the same send function used to call reducers. There are two important lifecycle events for views so you can trigger actions when a DOM node is added and removed from the DOM. These are onload and onunload. Here, as soon as the view is added to the DOM, we fire our fetchItems effect.

const itemView = (item) => html`<li>${item.name}</li>`

const myView = (state, prev, send) => html`
  <div onload=${() => send('fetchItems')}>
    <ul>
      ${state.items.map(item => itemView(item))}
    </ul>
  </div>
`

Run the example: npm run example-4

Read more in the docs: Effects

Subscriptions

Subscriptions are a way of receiving data from a source. For example when listening for events from a server using SSE or Websockets for a chat app, or when catching keyboard input for a videogame.

Subscriptions are registered at app.start. Here’s an example of using subscriptions to listen to key presses and store the pressed keys in state.

View file on GitHub: 5-subscriptions.js

const keyMap = {
  37: 'left',
  38: 'up',
  39: 'right',
  40: 'down'
}

app.model({
  state: {
    pressedKeys: {
      left: false,
      up: false,
      right: false,
      down: false
    }
  },
  subscriptions: [
    (send, done) => {
      function keyChange(keyCode, value) {
        const key = keyMap[keyCode]
        if (!key) return

        const patch = {}
        patch[key] = value
        send('updatePressedKeys', patch, done)
      }
      window.addEventListener('keydown', (event) => {
        keyChange(event.keyCode, true)
      }, false)
      window.addEventListener('keyup', (event) => {
        keyChange(event.keyCode, false)
      }, false)
    }
  ],
  reducers: {
    updatePressedKeys: (patch, state) => ({
      pressedKeys: Object.assign(state.pressedKeys, patch)
    })
  }
})

Run the example: npm run example-5

Read more in the docs: Subscriptions

Routing

Below you can see a more complete example of how routing works in Choo. Here app.router is a wrapper around the sheet-router package which supports default and nested routes. You can also programatically update the route with the location reducer: send('location:setLocation', { location: href }).

View file on GitHub: 6-routes.js

To link from view to view you can simply use links.

const homeView = (state, prev, send) => html`
  <div>
    <h1>Welcome</h1>
    <p>Check out your <a href="/inbox">Inbox</a></p>
  </div>
`

The routes themselves can be registered like so.

app.router(route => [
  route('/', homeView),
  route('/inbox', inboxView, [
    route('/:id', mailView),
  ])
])

Dynamic parts of the URL’s can be accessed via state.params

const mailView = (state, prev, send) => {
  const email = state.items.find(item => item.id === state.params.id)
  return html`
    <div>
      ${navView(state)}
      <h2>${email.subject}</h2>
      <p>${email.to}</p>
    </div>
  `
}

Run the example: npm run example-6

Read more in the docs: Router

Component State and Leaf Nodes

Choo views are designed to be pure functions that accept data and return DOM nodes. React has shown that this can be a great way to build declarative UIs but it has a downside. How can you include components to a Choo view that maintain their own state and modify their own DOM nodes? How can you can include impure components in Choo and leverage the vast number of DOM libraries out there?

Here’s a naive attempt at trying to include a d3 data visualization in a Choo view. The onload function is passed a reference to the DOM node that was added, we can successfully modify that element with d3, but on re-renders our viz is gone, forever…

const dataVizView = (state) => {
  function load(el) {
    d3.select(el)
      .selectAll('div')
      .data(state.data)
      .enter()
      .append('div')
      .style('height', (d)=> d + 'px')
  }

  return html`
    <div onload=${load}></div>
  `
}

The diffing library that Choo uses (morphdom) offers an escape hatch in isSameNode which can be used to prevent re-renders. Choo’s cache-element contains functions that wrap this behavior to simplify the code needed for caching and making widgets in Choo.

View file on GitHub: 7-friends.js

const widget = require('cache-element/widget')
const dataViz = widget(update => {
  update(onUpdate)

  const el = html`<div></div>`
  return el

  function onUpdate(state) {
    const bars = d3.select(el)
      .selectAll('div.bar')
      .data(state.data)

    bars.style('height', (d)=> d + 'px')

    bars.enter()
      .append('div')
      .attr('class', 'bar')
      .style('height', (d)=> d + 'px')
  }
})
const dataVizView = (state, prev, send) => dataViz(state)

Run the example: npm run example-7

We’ve now touched on all of the major components of Choo’s API, I told you it was tiny.

There’s also app.use to extend the way Choo works, allowing you to intercept its flow at different points like onAction and onStateChange and execute your own code. These hooks can be used to create plugins or middleware.

Additionally, Server-side rendering can be achieved with app.toString(route, state).

Unit Testing

One of the most touted merits of functional programming is testability, so how does Choo stack up?

Component Specs

Choo Views are pure functions that take state as input and return a DOM node, so they’re easy to test. Here’s how you could render a node and make assertions on it with Mocha and Chai.

const html = require('choo/html')
const myView = (state) => html`
  <div class="my-view">
    ${JSON.stringify(state)}
  </div>
`

describe('Component specs', () => {
  it('should return a DOM node', () => {
    const el = myView({hello: 'yep'})

    expect(el.innerHTML).to.contain('{"hello":"yep"}')
    expect(el.className).to.equal('my-view')
  })
})

Reducer Specs

Testing reducers is similar, they are functions that take state and a payload as input and return a new state. You’ll want to pull each reducer function out of the model so that you can test them independently.

const myReducer = (data, state) => {
  return { title: data }
}

describe('Reducer specs', () => {
  it('should reduce state', () => {
    const prev = { title: 'hello!' }
    const state = myReducer(prev, "🚂 Choo!")

    expect(state.title).to.equal("🚂 Choo!")
  })
})

These are just examples to show what the unit testing story could look for Choo apps. Each of the concepts are implemented with pure functions, so can easily be tested in isolation.

Strengths

It’s simple and cohesive. The predictable flow between routes, views, actions, and reducers makes it simple to learn and fun to work with. The tiny API means that once you know how those components work together, you can start building without looking at detailed docs.

Little tooling required. There’s no need for JSX or complex build pipelines, browserify is all that’s recommended to pull the dependencies together into a bundle. That can be as simple as browserify ./entry.js -o ./bundle.js.

It’s disposable. Building a part of your app in Choo is not a life sentence. The views are simply functions that return DOM nodes so they can be used anywhere that works with the DOM.

The minimal 5kb footprint means that you can include other versions of Choo or other frameworks without worry. It’s a framework on a diet.

Weaknesses

It’s immature and will have breaking changes. See the v4 change log for an example of how the API is a moving target. Whilst progress is a great thing, working on migrations between versions is a potential downside.

You may need to manually optimize. Larger libraries like React and Angular that expect to own the whole app can do things like optimizing events with delegation at the top of the tree. yo-yo doesn’t have the luxury, if you want event delegation you’re going to need to understand how it works and implement it yourself by registering events at a top level component.

It’s not battle tested. When you adopt a library like React, you can do so with confidence knowing that it’s used on some of the largest sites on the web. You know it’s been thoroughly tested and will work predictably, even in old browsers.

It’s the new kid on the block. Popularity means that you can leverage a network of knowledge and utilize off-the-shelf components. The last example shows how you include other libraries that manipulate parts of the DOM inside a view but it’s still pretty raw at this stage. The patch that enabled this was only a few days old at the time of writing this article.

Conclusion

Personally, I like Choo a lot. It brings together a lot of great ideas with a friendly API. I can see myself using it on side projects at first to see how it works across a wide range of scenarios. I expect to reach limitations at some point but them’s the breaks when you work at the bleeding edge.

If this has piqued your interest you may want to read through the README, explore the demos or read the work-in-progress Handbook for more examples from the author.

What do you think? Give it a try and let us know how you get on in the comments below.