choo 
:steam_locomotive::train::train::train::train:🚋 - The little framework that could.
A framework for creating sturdy web applications. Built on years of industry experience it distills the essence of functional architectures into a productive package.
Features
- minimal size: weighing under
8kb,choois a tiny little framework - single state: immutable single state helps reason about changes
- small api: with only 5 methods, there's not a lot to learn
- minimal tooling: built for the cutting edge
browserifycompiler - transparent side effects: using "effects" and "subscriptions" brings clarity to IO
- omakase: composed out of a balanced selection of open source packages
- idempotent: renders seemlessly in both Node and browsers
- very cute: choo choo!
Demos
- Input example (@examples directory)
- HTTP effects example (@examples directory)
- Mailbox routing example (@examples directory)
Usage
const choo = require('choo')
const app = choo()
app.model('title', {
state: {
title: 'my-demo-app'
},
reducers: {
'update': (action, state) => ({ title: action.payload })
},
effects: {
'update': (action, state, send) => (document.title = action.payload)
}
})
const mainView = (params, state, send) => choo.view`
<main class="app">
<h1>${state.title}</h1>
<label>Set the title</label>
<input
type="text"
placeholder=${state.title}
oninput=${(e) => send('title:update', { payload: e.target.value })}>
</main>
`
app.router((route) => [
route('/', mainView)
])
const tree = app.start()
document.body.appendChild(tree)
Concepts
- user: 🙆
- DOM: the Document Object Model is what is currently displayed in your browser
- actions: a named event with optional properties attached. Used to call
effectsandreducersthat have been registered inmodels - model: optionally namespaced object containing
subscriptions,effectsandreducers - subscriptions: read-only data sources that emit
actions - effects: asynchronous functions that emit an
actionwhen done - reducers: synchronous functions that modify
state - state: a single object that contains all the values used in your application
- router: determines which
viewto render - views: take
stateand returns a newDOM treethat is rendered in the browser
┌───────────────────────────┐ ┌────────┐
│ ┌─────────────────┐ │ │ User │
├────│ Subscriptions │ │ └────────┘
│ ├─────────────────┤ │ │
└────│ Effects │◀───┤ ▼
├─────────────────┤ Actions ┌────────┐
│ Reducers │◀───┴─────│ DOM │
Models──────────────┘ └────────┘
│ ▲
State DOM│tree
▼ │
┌────────┐ ┌────────┐
│ Router │─────State ───▶│ Views │
└────────┘ └────────┘
Effects
Side effects are done through effects declared in app.model(). Unlike
reducers they cannot modify the state by returning objects, but get a
callback passed hich is used to emit actions to handle results.
A typical effect flow looks like:
- An action is received
- An effect is triggered
- The effect performs an async call
- When the async call is done, either a success or error action is emitted
- A reducer catches the action and updates the state
HTTP
choo ships with a built-in http module
that weighs only 2.4kb:
const http = require('choo/http')
// GET JSON
http.get('/my-endpoint', { json: true }, function (err, res, body) {
if (err) throw err
if (res.statusCode !== 200 || !body) throw new Error('something went wrong')
})
// POST JSON
const body = { foo: 'bar' }
http.post('/my-endpoint', { json: body }, function (err, res, body) {
if (err) throw err
if (res.statusCode !== 200 || !body) throw new Error('something went wrong')
})
// DELETE
http.del('/my-endpoint', function (err, res) {
if (err) throw err
if (res.statusCode !== 200) throw new Error('something went wrong')
})
Note that http only runs in the browser to prevent accidental requests when
rendering in Node. For more details view the raynos/xhr
documentation.
Subscriptions (wip)
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.
Server Sent Events (SSE)
[tbi] - help and suggestions welcome!
Keyboard
[tbi] - help and suggestions welcome!
Websockets
[tbi] - help and suggestions welcome!
API
app = choo()
Create a new choo app
app.model(name?, obj)
Create a new model. Models modify data and perform IO. Obj takes the following arguments:
- state: object. Key value store of initial values
- reducers: object. Syncronous functions that modify state. Each function
has a signature of
(action, state) - effects: object. Asyncronous functions that perform IO. Each function has
a signature of
(action, state, send)wheresendis a reference toapp.send()
If a name string is passed as a first argument, reducers and signatures
will be prefixed by the name. So if name is "user" and a reducer called
"update" is registered, it would be accessed as 'user:update' in send().
choo.view`html`
Tagged template string HTML builder. See
yo-yo for full documentation. Views
should be passed to app.router()
app.router(params, state, send)
Creates a new router. See
sheet-router for full
documentation. Registered views have a signature of (params, state, send),
where params is URI partials.
tree = app.start()
Start the application. Returns a DOM element that can be mounted using
document.body.appendChild().
Packages used
- views:
yo-yo - models:
send-action,xtend - routes:
sheet-router - http:
xhr
Optimizing
To bring down file size, consider running the following browserify
transforms:
- unassertify - remove
assert()statements which reduces file size. Use as a--globaltransform - varify - replace
constwithvarstatements. Use as a--globaltransform - uglifyify - minify your code using
UglifyJS2. Use as a
--globaltransform
Packages that work well together
FAQ
How does choo compare to X?
- react: [tbi]
- mithril: [tbi]
- preact: [tbi]
- angular2: [tbi]
Installation
$ npm install choo