NexusCS

Choo

JavaScript libraries
Choo is a 4kb functional framework for building single-page applications with minimal API and real DOM diffing.
framework
spa
functional

Getting started

Introduction

Choo is a 4kb framework for building SPAs with functional programming patterns. Uses real DOM diffing (nanomorph) and event-based architecture with just 6 API methods.

npm install choo

Basic app

var html = require("choo/html");
var choo = require("choo");

var app = choo();
app.use(countStore);
app.route("/", mainView);
app.mount("body");

Minimal setup with store and route.

View function

function mainView(state, emit) {
  return html`
    <body>
      <h1>count is ${state.count}</h1>
      <button onclick=${onclick}>Increment</button>
    </body>
  `;

  function onclick() {
    emit("increment", 1);
  }
}

Views receive state and emit function.

Store function

function countStore(state, emitter) {
  state.count = 0;

  emitter.on("increment", function (count) {
    state.count += count;
    emitter.emit("render");
  });
}

Stores manage state and listen to events.

API methods

Initialize

Method Description
choo([opts]) Create new app instance
var app = choo();

Default configuration.

var app = choo({
  history: true, // History API enabled
  href: true, // Handle <a> clicks
  hash: false, // Hash routing disabled
  cache: 100, // Component cache size
});

With custom options.

Register store

Method Description
app.use(callback) Register a store
app.use(function (state, emitter, app) {
  state.myData = [];

  emitter.on("fetch", function () {
    fetch("/api/data")
      .then((res) => res.json())
      .then((data) => {
        state.myData = data;
        emitter.emit("render");
      });
  });
});

Store with async data fetching.

function myStore(state, emitter, app) {
  // Store logic
}
myStore.storeName = "myStore";
app.use(myStore);

Named store for debugging.

Register route

Method Description
app.route(path, handler) Register route and view
app.route("/", homeView);
app.route("/about", aboutView);
app.route("/users/:id", userView);
app.route("/files/*", filesView);
app.route("*", notFoundView);

Route patterns with params and wildcards.

Mount and start

Method Description
app.mount(selector) Mount on DOM element
app.start() Start app, return tree
app.toString(path, state) Render to string (SSR)
app.mount("body");

Mount on existing DOM node.

var tree = app.start();
document.body.appendChild(tree);

Manual DOM insertion.

var html = app.toString("/", { name: "Node" });

Server-side rendering to HTML string.

Views and templates

HTML templates

var html = require("choo/html");

function myView(state, emit) {
  return html`
    <div class="container">
      <h1>${state.title}</h1>
      <button onclick=${handleClick}>Click Me</button>
    </div>
  `;

  function handleClick() {
    emit("update-title", "New Title");
  }
}

Template literals with tagged html.

Conditional rendering

function view(state, emit) {
  return html`
    <div>
      ${state.loaded
        ? html`<p>Content: ${state.content}</p>`
        : html`<p>Loading...</p>`}
    </div>
  `;
}

Use ternary expressions for conditionals.

Lists

function listView(state, emit) {
  return html`
    <ul>
      ${state.items.map((item) => html` <li id=${item.id}>${item.name}</li> `)}
    </ul>
  `;
}

Use IDs for efficient reordering.

Attributes

html`
  <input
    type="text"
    value=${state.value}
    oninput=${(e) => emit("input", e.target.value)}
  />
`;

Dynamic attributes and event handlers.

State object

Built-in properties

Property Type Description
state.events Object Built-in event names
state.params Object Route parameters
state.query Object Query string params
state.href String Current href (no query)
state.route String Current route pattern
state.title String Page title
state.components Object Component state
state.cache() Function LRU component cache

Route parameters

app.route("/users/:id", userView);

function userView(state, emit) {
  var userId = state.params.id;
  return html`<h1>User ${userId}</h1>`;
}

Access params from state.params.

Query parameters

// URL: /search?q=choo&page=2
function searchView(state, emit) {
  var query = state.query.q; // 'choo'
  var page = state.query.page; // '2'
  return html`<h1>Search: ${query}</h1>`;
}

Access query string from state.query.

Custom state

function myStore(state, emitter) {
  state.user = null;
  state.isLoggedIn = false;
  state.todos = [];
}

Add custom properties in stores.

Events

Built-in events

Event Constant When
'DOMContentLoaded' state.events.DOMCONTENTLOADED DOM ready
'render' state.events.RENDER Trigger re-render
'navigate' state.events.NAVIGATE Route changed
'pushState' state.events.PUSHSTATE Navigate (history add)
'replaceState' state.events.REPLACESTATE Navigate (history replace)
'popState' state.events.POPSTATE User hit back
'DOMTitleChange' state.events.DOMTITLECHANGE Update page title

Emitting events

function view(state, emit) {
  function onClick() {
    emit("custom-event", { data: "value" });
  }

  return html` <button onclick=${onClick}>Trigger</button> `;
}

Emit custom events with data.

function view(state, emit) {
  function navigate() {
    emit("pushState", "/about");
  }

  return html` <button onclick=${navigate}>Go to About</button> `;
}

Navigate programmatically.

Listening to events

function myStore(state, emitter) {
  emitter.on("DOMContentLoaded", function () {
    emitter.on("custom-event", handleEvent);
  });

  function handleEvent(data) {
    state.value = data;
    emitter.emit("render");
  }
}

Listen in stores, emit render to update.

Event constants

function myStore(state, emitter) {
  emitter.on(state.events.DOMCONTENTLOADED, init);
  emitter.on(state.events.NAVIGATE, onNavigate);

  function init() {
    // Initialization
  }

  function onNavigate() {
    // Route changed
  }
}

Use constants for built-in events.

Routing

Route patterns

app.route("/", homeView);
app.route("/about", aboutView);
app.route("/users/:id", userView);
app.route("/blog/:year/:month/:slug", postView);
app.route("/files/*", filesView);
app.route("*", notFoundView);

Static, params, wildcard, and catch-all.

Navigation

// Add to history
emit("pushState", "/about");

// Replace history entry
emit("replaceState", "/login");

// With query string
emit("pushState", "/search?q=choo");

Navigate with pushState or replaceState.

html` <a href="/about">About</a> `;

Links are intercepted automatically.

html`
  <a href="https://external.com" target="_blank" rel="noopener noreferrer">
    External
  </a>
`;

External links with target attribute.

Hash routing

var app = choo({ hash: true });

app.route("#/", homeView);
app.route("#/about", aboutView);

Use hash routing instead of history API.

Route events

function routeStore(state, emitter) {
  emitter.on("navigate", function () {
    console.log("Route:", state.route);
    console.log("Params:", state.params);
    console.log("Query:", state.query);
  });
}

Listen to navigate event for route changes.

Components

Nanocomponent

var Nanocomponent = require("choo/component");
var html = require("choo/html");

class Button extends Nanocomponent {
  constructor() {
    super();
    this.color = null;
  }

  createElement(color) {
    this.color = color;
    return html` <button style="background: ${color}">Click Me</button> `;
  }

  update(newColor) {
    return newColor !== this.color;
  }
}

Class-based component with lifecycle.

Using components

function view(state, emit) {
  var button = state.cache(Button, "my-button");

  return html` <div>${button.render("green")}</div> `;
}

Cache and render components.

Lifecycle methods

Method Description
createElement(args) Create element (required)
update(args) Return true to re-render
beforerender(el) Before createElement/update
load(el) After mounted to DOM
unload(el) Before removed from DOM
afterupdate(el) After update renders
afterreorder(el) After element reordered
class MyComponent extends Nanocomponent {
  load(el) {
    console.log("Mounted:", el);
  }

  unload(el) {
    console.log("Unmounted:", el);
  }
}

Lifecycle hooks for side effects.

Component methods

Method Description
component.render(args) Call createElement or update
component.rerender() Force re-render
component.element Current DOM element
var button = state.cache(Button, "btn");
button.render("red");

Render with arguments.

Server-side rendering

Render to string

var app = require("./app");

var state = {
  name: "Node",
  user: { id: 123 },
};

var html = app.toString("/", state);

var page = `
<!DOCTYPE html>
<html>
  <head>
    <title>My App</title>
  </head>
  <body>
    ${html}
    <script>
      window.initialState = ${JSON.stringify(state)}
    </script>
    <script src="/bundle.js"></script>
  </body>
</html>
`;

Server-side render with initial state.

Rehydration

var app = choo();

if (typeof window !== "undefined" && window.initialState) {
  Object.assign(app.state, window.initialState);
}

app.use(myStore);
app.route("/", mainView);
app.mount("body");

Rehydrate from initial state on client.

Optimization

List keys

function listView(state, emit) {
  return html`
    <ul>
      ${state.items.map(
        (item) => html` <li id=${item.id} key=${item.id}>${item.name}</li> `,
      )}
    </ul>
  `;
}

Use IDs for efficient list reordering.

Skip diffing

function view(state, emit) {
  var staticNode = html`<div>Static content</div>`;
  staticNode.isSameNode = () => true;

  return html` <div>${staticNode}</div> `;
}

Mark nodes to skip diffing.

Production build

# Using bankai
bankai build index.js

# Browserify transforms
browserify \
  -t unassertify \
  -t nanohtml \
  -g uglifyify \
  index.js > bundle.js

Optimize for production.

Common patterns

Async data loading

function dataStore(state, emitter) {
  state.data = [];
  state.loading = false;

  emitter.on("DOMContentLoaded", function () {
    emitter.on("load-data", loadData);
  });

  function loadData() {
    state.loading = true;
    emitter.emit("render");

    fetch("/api/data")
      .then((res) => res.json())
      .then((data) => {
        state.data = data;
        state.loading = false;
        emitter.emit("render");
      })
      .catch((err) => {
        state.error = err.message;
        state.loading = false;
        emitter.emit("render");
      });
  }
}

Loading states with error handling.

Form handling

function formView(state, emit) {
  return html`
    <form onsubmit=${onSubmit}>
      <input
        type="text"
        value=${state.formData.name}
        oninput=${(e) => updateField("name", e.target.value)}
      />
      <button type="submit">Submit</button>
    </form>
  `;

  function updateField(field, value) {
    emit("update-field", { field, value });
  }

  function onSubmit(e) {
    e.preventDefault();
    emit("submit-form", state.formData);
  }
}

Controlled form inputs.

Gotchas

State access

⚠️ state.params and state.query are plain objects

// ✅ Correct
var userId = state.params.id;

// ❌ Wrong - they're NOT Promises
var userId = await state.params.id;

Access route/query params directly.

Component unmounting

⚠️ Components lose state when unmounted

// ❌ Wrong - component unmounts
${showButton ? button.render() : null}

// ✅ Correct - keep mounted
${button.render(showButton)}

Keep components mounted to maintain control.

Root node consistency

⚠️ createElement must return same root type

// ❌ Wrong - changes root type
createElement (type) {
  return type === 'button'
    ? html`<button>Click</button>`
    : html`<div>Text</div>`
}

// ✅ Correct - consistent root
createElement (type) {
  return html`
    <div>
      ${type === 'button'
        ? html`<button>Click</button>`
        : html`<span>Text</span>`
      }
    </div>
  `
}

Wrap in consistent container element.

Back navigation

⚠️ emit('popState') doesn't navigate back

// ❌ Wrong - popState is an event
emit("popState");

// ✅ Correct - use browser API
window.history.go(-1);
// or
window.history.back();

Use browser history API for back navigation.

External links

⚠️ External links need rel attribute

// ❌ Wrong - Choo will intercept
html`<a href="https://external.com" target="_blank">Link</a>`;

// ✅ Correct - ignored by Choo
html`
  <a href="https://external.com" target="_blank" rel="noopener noreferrer">
    Link
  </a>
`;

Add rel attribute for external links.

Also see