featherweight

featherweight application pattern


Keywords
application, lightweight
License
MIT
Install
npm install featherweight@3.0.0

Documentation

featherweight

PLEASE NOTE: This is all highly experimental, I've not built anything major with this and there are holes in functionality and unsolved problems related to events. Also, just because I'm open sourcing doesn't mean I'll maintain it for you or do anything else with it, ever. This is a pure experiment at this point. I'm simply sharing it in the spirit of open source in case anyone finds it interesting.

No Maintenance Intended

The basic idea of featherweight is that you as a developer only do this:

  1. Configure redux however you want it (all your application state will live in Redux)
  2. Write a single, main, pure, synchronous view function that takes the state from redux (including the current URL of your app) and returns a virtual dom to represent what it should look like at that exact moment.

That's it!

Featherweight then manages everything else. It's a simple abstraction to manage communication between the worker thread (where Redux and your reducers will run) and the main thread:**

  1. 99% of your app code runs in a web worker
  2. No fancy router, the url is just another piece of state
  3. You can super easily pre-render as much HTML as possible before sending to the client either on the server or to static HTML files at build time.
  4. JS "takes over" on the clientside as soon as it's loaded.
  5. All code necessary to do the above should be < 10kb min + gzipped

understanding the application pattern

Featherweight lets you do all the heavy application work in a web worker. This means rendering new UI, virtual dom diffing, data fetching, etc. all happens in a Web Worker.

This leaves the main thread free to focus entirely on efficient DOM updates and listening for user interractions.

It consists of two primary components:

  1. The worker code: import { worker } from 'featherweight'
  2. The main thread (ui thread) code: import { ui } from 'featherweight'

Setting up the main thread

Featherweight's ui handles most of the main thread for you. The only custom code you need here, may be to import whatever styles or 3rd party scripts.

Typically, it would just look like this:

// import the ui module
import { ui } from 'featherweight'

// import your worker code (with webpack-worker-loader this Just Works™)
import WorkerThread from './worker.thread'

// possibly import styles if you're using some sort of css/style loader with webpack
import './styles/main.styl'

ui({
  // pass it the worker thread
  worker: new WorkerThread(),

  // Pass in the root element where you
  // want the app to live. It's recommended
  // that this be something other than the
  // <body> in case other libraries or
  // browser plugins need to insert elements, etc,
  rootElement: document.body.firstChild
})

setting up the worker thread

Here's where all the work happens, but again, most of your efforts will go into writing the Redux reducers and UI code.

The boilerplate inside the worker is pretty straight forward:

// worker.thread.js
import { worker } from 'featherweight'
import appView from './views/app'
import configureRedux from './configureRedux'

worker({
  // pass it your redux main store
  // here we assume we're setting up
  // redux and it's reducers in another module
  // that exports a function we can use to
  // get the configured redux instance
  redux: configureRedux(),

  // This is the main application UI component that
  // you'll write. It should be a pure function returning
  // a new virtual DOM when passed the main application state
  // object we get from Redux. 
  view: appView,

  // To keep things easy to trace we also want to 
  // explicitily  pass in the reference to the worker
  // context. This will always just be `self`. 
  // 
  // If you're unfamiliar with this concept, it's just 
  // something that exists within all Web Workers. 
  // 
  // featherweight needs this in order to be able to listen for
  // and send DOM updates back to the ui thread.
  workerContext: self
})

Writing the UI components

The assumption is that components don't have state. All state lives in Redux, including the current URL of the app.

This means the entire UI needs to be a single pure function that takes the application state object and return a new virtual dom.

It's signature is simple:

virtualDom = ui(state)

Of course, you still have to be able to break your application into small, modular components and show different things based on different urls.

To address this, here's a super simple example:

import home from './home'
import about from './about'
import pageNotFound from './pageNotFound'

export default (state) => {
  // we just grab the `.url` property of state
  const { url } = state
  let page

  // We then grab a `page` component
  // conditionally based on that url
  if (url === '/') {
    // here we can pass the state through
    // if we'd like
    page = home(state)
  } else if (url === '/about') {
    // this page is just text content
    // so passing in state isn't necessary
    page = about()
  }

  // we could also handle URLs our app isn't
  // aware of with a fallback page
  if (!page) {
    page = pageNotFound()
  }

  // here we simply return the JSX and include
  // the `{page}` content as part of our layout
  return (
    // note that `main` here is just an HTML5 element
    // nothing special
    <main>
      <h1>Feather POC App</h1>
      <nav>
        <a href='/'>home</a> | <a href='/about'>about</a>
      </nav>
      {page}
    </main>
  )
}

handling state

For cleanliness, I suggest setting up your Redux in a separate file.

That file may look something like this:

import { createStore } from 'redux'
import * as reducers from './reducers/index'

export default () => {
  return createStore(reducers)
}

pre-rendering at build time or as part of a server response

Because your app UI is a pure function, turning your view into HTML is quite simple.

Simply call your apps main UI function passing in whatever state you'd like to render to generate virtual DOM.

Then you can use the vdom-to-html module from npm to generate an HTML string.

import toHtml from 'vdom-to-html'
import app from './views/app'

// wherever you want to create the HTML string
const renderedHtml = toHtml(app({url: '/about'}))

featherweight patterns

  1. Under no circumstances should your complete app weigh more than 60 kb min+gzip JS. Ideally, much less.
  2. every. single. piece. of. state. lives. in. redux
  3. Leave the main thread alone
  4. Never touch the DOM directly
  5. The UI thread does nothing other than apply DOM updates and post events back to the worker thread
  6. Never use this
  7. Never use function use () => {} for everything
  8. The UI is a pure function
  9. There are no stateful components
  10. Pretty much everything is a pure function
  11. Name all your modules with camelCase file names
  12. Use standard for code style
  13. The build step should turn your app into a set of static HTML, CSS, and JS files.

install

npm install featherweight

credits

If you like this follow @HenrikJoreteg on twitter.

license

MIT