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!
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.
Frequently Asked Questions about Functional Programming with Choo
What is functional programming and how does it differ from other programming paradigms?
Functional programming is a programming paradigm that treats computation as the evaluation of mathematical functions and avoids changing-state and mutable data. It emphasizes the application of functions, in contrast to the imperative programming style, which emphasizes changes in state. Functional programming allows for easier testing and debugging, as well as more predictable code, as functions have no side effects.
What is Choo and how does it relate to functional programming?
Choo is a minimalist JavaScript framework for building robust and maintainable applications. It is designed with functional programming principles in mind, meaning it encourages the use of pure functions and avoids changing-state and mutable data. This makes your code more predictable and easier to understand and debug.
How does Choo compare to other JavaScript frameworks?
Choo is a lightweight and minimalist framework, making it a good choice for smaller projects or for developers who prefer a less complex tool. It is designed with functional programming principles in mind, which can make your code more predictable and easier to debug. However, it may not have as many built-in features as larger frameworks like Angular or React.
How can I get started with functional programming in Choo?
To get started with functional programming in Choo, you’ll first need to have a basic understanding of JavaScript and functional programming principles. From there, you can install Choo using npm and start building your application. The Choo documentation provides a good starting point for learning how to use the framework.
What are some best practices for functional programming in Choo?
Some best practices for functional programming in Choo include keeping your functions pure (i.e., they should not have side effects), avoiding mutable data, and using higher-order functions where possible. It’s also a good idea to keep your code modular and well-organized, as this can make it easier to understand and maintain.
Can I use Choo with Node.js?
Yes, Choo can be used with Node.js. In fact, Choo is built on top of the Node.js runtime, so you can use all the Node.js modules and features in your Choo application.
What are some common challenges when using functional programming in Choo and how can I overcome them?
Some common challenges when using functional programming in Choo include understanding the functional programming paradigm, dealing with asynchronous code, and managing state. To overcome these challenges, it can be helpful to read up on functional programming principles, use promises or async/await for asynchronous code, and use state management libraries or techniques.
Are there any resources or tutorials for learning functional programming in Choo?
Yes, there are several resources available for learning functional programming in Choo. The Choo documentation is a great place to start, and there are also several tutorials and blog posts available online. Additionally, there are several online communities where you can ask questions and get help.
How does functional programming in Choo affect performance?
Functional programming in Choo can actually improve performance in some cases. Because functional programming avoids changing-state and mutable data, it can reduce the risk of bugs and make your code more predictable. Additionally, Choo is a lightweight framework, so it can be faster and more efficient than larger, more complex frameworks.
Can I use functional programming in Choo for large-scale applications?
Yes, you can use functional programming in Choo for large-scale applications. While Choo is a lightweight and minimalist framework, it is also robust and scalable, making it suitable for both small and large projects. However, for very large or complex applications, you may want to consider using a larger framework like Angular or React.
Hello. I'm a front end web developer from Melbourne, Australia. I enjoy working on the web, appreciate good design and working along side talented people I can learn from. I have a particular interest in visual programming so have fun working with SVG and Canvas.