2016-05-22 14:18:23 +09:00
2016-05-22 02:06:46 +09:00
2016-05-21 15:32:16 +09:00
.
2016-05-11 01:09:53 +07:00
.
2016-05-11 01:09:53 +07:00
2016-05-13 12:24:51 +07:00
2016-05-22 02:06:46 +09:00
.
2016-05-11 01:09:53 +07:00
2016-05-22 02:08:23 +09:00
2016-05-22 14:18:23 +09:00
.
2016-05-11 01:09:53 +07:00

choo stability

npm version build status test coverage downloads js-standard-style

: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 7kb, choo is a tiny little framework
  • single state: immutable single state helps reason about changes
  • small api: with only 6 methods, there's not a lot to learn
  • minimal tooling: built for the cutting edge browserify compiler
  • 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

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 effects and reducers that have been registered in models
  • model: optionally namespaced object containing subscriptions, effects and reducers
  • subscriptions: read-only data sources that emit actions
  • effects: asynchronous functions that emit an action when done
  • reducers: synchronous functions that modify state
  • state: a single object that contains all the values used in your application
  • router: determines which view to render
  • views: take state and returns a new DOM tree that 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:

  1. An action is received
  2. An effect is triggered
  3. The effect performs an async call
  4. When the async call is done, either a success or error action is emitted
  5. 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}`)
  }
})

Rendering in Node

Sometimes it's necessary to render code inside of Node; for serving first requests, testing or other purposes. Applications that are capable of being rendered in both Node and the browser are called isomorphic.

Rendering in Node is slightly different than in the browser. First off, to maintain performance all calls to subscriptions, effects, and reducers are disabled. That means you need to know what the state of your application is going to be before you render it - no cheating!

Secondly, the send() method inside router and view has been disabled. If you call it your program will crash. Disabling all these things means that your program will render O(n), which is super neat. Off to 10.000 QPS we go!

To render in Node call the .toString() method instead of .start(). The first argument is the path that should be rendered, the second is the state:

const http = require('http')
const client = require('./client')  // path to client entry point
http.createServer(function (req, res) {
  const html = client.toString('/', { message: 'hello server!' })
  res.setHeader('Content-Type', 'text/html; charset=utf-8')
  res.end(html)
})

In order to make our choo app call app.start() in the browser and be require()-able in Node, we check if module.parent exists:

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

app.router((route) => [
  route('/', (params, state, send) => choo.view`
    <h1>${state.message}</h1>
  `)
])

if (module.parent) module.exports = app
else document.body.appendChild(app.start())

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) where send is a reference to app.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.

html = app.toString(route, state)

Render the application to a string. Useful for rendering on the server. First argument is a path that's passed to the router. Second argument is the state object. When calling .toString() instead of .start(), all calls to send() are disabled, and subscriptions, effects and reducers aren't loaded.

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?

Ah, so this is where I get to rant. choo (chugga-chugga-chugga-choo-choo!) was built because other options didn't quite cut it for me, so instead of presenting some faux-objective chart with skewed benchmarks and checklists I'll give you my opinions directly instead. Ready? Here goes:

  • react: react is kind of big (155kb was it?), has a lot of new, odd words and does weird things with versioning. They also like classes a lot, and enforce a lot of abstractions. It also encourages the use of JSX and babel which break JavaScript, The Language™. And all that without even making clear how code should flow, which is bad in a team setting. I don't like complicated things and in my view react is one of them. react is not for me.
  • mithril: never used it, never will. I didn't like the API, but if you like it maybe it's worth a shot - the API seems small enough. I wouldn't know how pleasant it is past face value.
  • preact: a pretty cool idea; seems to fix most of what is wrong with react - except what is broken by design (the API). It also doesn't fix the large dependencies react seems to use (e.g. react-router and friends). If react is your jam, and you will not budge, sitting at 3kb this is probably a welcome gift.
  • angular: definitely not for me. I like small things with a clear mental model; angular doesn't tick any box in my book of nice things.
  • angular2: I'm not sure what's exactly changed, but I know the addition of TypeScript and RxJS definitely hasn't made things simpler. Last I checked it was ~200kb in size before including some monstrous extra deps. I guess angular and I will just never get along.
  • mercury: ah, mercury is an interesting one. It seemed like a brilliant idea until I started using it - the abstractions felt heavy, and it took team members a long time to pick up. In the end I think using mercury helped greatly in getting choo where it is now.
  • deku: deku is fun. I even contributed a bit in the early days. It could probably best be described as "a functional version of react". The dependence on JSX isn't great, but give it a shot if you think it looks neat.

Which packages was choo built on?

Does choo use a virtual-dom?

choo uses morphdom, which diffs real DOM nodes instead of virtual nodes. It turns out that browsers are actually ridiculously good at dealing with DOM nodes, and it has the added benefit of working with any library that produces valid DOM nodes. So to put a long answer short: we're using something even better.

What packages do you recommend to pair with choo?

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 --global transform
  • varify - replace const with var statements. Use as a --global transform
  • uglifyify - minify your code using UglifyJS2. Use as a --global transform

Is it production ready?

Sure.

Installation

$ npm install choo

License

MIT

S
Description
A modern fork of choo using vite
Readme MIT
873 KiB
Languages
JavaScript 99.7%
TypeScript 0.3%