11 KiB
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 which is used to emit actions to handle results. Use effects
every time you don't need to modify the state object directly, but wish to
respond to an action.
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
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)
Server Sent Events (SSE) allow servers to push data to the browser.
They're the unidirectional cousin of websockets and compliment HTTP
brilliantly. To enable SSE, create a new EventSource, point it at a local
uri (generally /sse) and setup a subscription:
const stream = new document.EventSource('/sse')
app.model({
subscriptions: [
function (send) {
stream.onerror = (e) => send('error', { payload: JSON.stringify(e) })
stream.onmessage = (e) => send('print', { payload: e.data })
}
],
effects: {
'sse:close': () => stream.close()
error: (state, event_ => console.error(`error: ${event.payload}`)),
print: (state, event) => console.log(`pressed key num: ${event.payload}`)
}
})
Keyboard
Most browsers have basic support for keyboard events. To
capture keyboard events, setup a subscription:
app.model({
subscriptions: [
function (send) {
keyboard.onkeypress = (e) => send('print', { payload: e.keyCode })
}
],
effects: {
print: (state, event) => console.log(`pressed key num: ${event.payload}`)
}
})
WebSockets
WebSockets allow for bidirectional communication between servers and browsers:
const socket = new document.WebSocket('ws://localhost:8081')
app.model({
subscriptions: [
function (send) {
socket.onerror = (e) => send('error', { payload: JSON.stringify(e) })
socket.onmessage = (e) => send('print', { payload: e.data })
}
],
effects: {
'ws:close': () => socket.close(),
'ws:send': (state, event) => socket.send(JSON.stringify(event.payload)),
error: (state, event_ => console.error(`error: ${event.payload}`)),
print: (state, event) => console.log(`pressed key num: ${event.payload}`)
}
})
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().
FAQ
How does choo compare to X?
- react: [tbi]
- mithril: [tbi]
- preact: [tbi]
- angular2: [tbi]
Which packages was choo built on?
- views:
yo-yo - models:
send-action,xtend - routes:
sheet-router - http:
xhr
What packages do you recommend to pair with choo?
- tachyons - functional CSS for humans
- sheetify - modular CSS bundler for browserify
- pull-stream - minimal streams
How can I optimize choo?
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
Installation
$ npm install choo