bourbon.js

bourbon.js is a reactive frontend framework written in under 100 lines and less than 2.5 kB (1.5 kB minified) of vanilla JS. It is designed to be included as a <script /> tags - without any build system - and be used directly from JavaScript.

Despite the tiny size, it offers the core of a reactive frontend framework in a convenient syntax. For example, a page with a button click counter can be written as follows:

// view is a function to declare components
import { view, tags } from "../bourbon.js"
// tags enables nice syntax for HTML generation
const { h1, div, p, button } = tags

// Here is our button counter component
const Counter = view(
	({ count }, update) =>
		// It has a state count which we initialize to zero
		(
			(count ??= 0),
			button({ onclick: () => update({ count: count + 1 }) }, `Count: ${count}`)
		),
)

// Here is a simple root view to add some meat to the page,
// and showcase the HTML generation syntax.
//
// The first (optional) argument to the tag function is an object
// to set any attribute, the second the content of the node.
const Root = view(() => [
	h1("Hello, World!"),
	div(
		{ style: "padding: 16px; background-color: wheat;" },
		p("I am a paragraph."),
		// bourbon.js for simplicity does not make any distinction between state and
		// input props, so we can initialize the count to 3
		Counter({ count: 3 }),
	),
])

// Attach the page to the body!
document.body.append(...Root())

From the example above, the core features of bourbon.js are apparent:

  • Components are declared by wrapping our component function with view().
  • A component takes as parameters a state and an update() function.
  • Components simply return a DOM element or a list of DOM elements to be injected at the call site.
  • The state can be used both to inject input parameters from the parent component or to define internal state (by default uninitialized). There is no distinction between input params and internal state.
  • update() can be called to re-render the component. Additionally, update can take a new state for the component. The state updates can be partial.
  • To use a component, simply call it as a function. The first parameter can optionally be an initial state, followed by child components.
  • Strings are treated as DOM Text notes when passed as children.
  • bourbon.js comes with the full set of HTML elements which can be used as building blocks and use the same syntax as custom components. These built-in HTML elements take as input any HTML attribute you want.
  • You can simply append the output of any component as DOM node where you prefer. It's therefore possible to use bourbon.js for only a piece of a page or even for multiple independent parts of a page. bourbon.js never changes anything DOM outside the root component(s).

Check the example folder for more advanced and standalone examples, including how to handle async data loading.

Code size optimizations

bourbon.js is designed to be used unminified and still be ultra-lightweight, therefore the code is written with size optimization in mind. Code readability and hackability are still of the utmost importance, so we only do size optimizations if they don't impact readability too heavily.

Generally, keeping features and edge-case support at a minimum is the main way the code is kept compact. For example, bourbon.js does not support manually creating HTML elements, the provided components must be used.

Also, separate modules are provided for node, testing and extras, so that you can pick only the parts you need.

Here are some of the optimizations performed:

  • No ; to terminate statements
  • In rare cases ; is used to put two statements on the same line (e.g. pi++; ni++)
  • Use of let instead of const
  • Multi-variable let statements are used, using one variable per line.
  • Bare for/if statements, without { if they are one-liner, but newlines are used for readability
  • Short variable names, if the variable context is local, single characters are used, in other cases short names such as view, fn, el, kids (in place of children) and k/v (for key/value) are preferred for internal symbols. Names considered too cryptic are skipped.
  • Tabs are used for indentation in place of spaces.
  • The ternary operator is sometimes used for short conditionals
  • Assignment is used as an expression, for example return dom = ...
  • Arrow functions without redundant () are used for single arguments
  • Only the necessary comments are used, we rather rely on external documentation and simple code.
  • We take advantage of all possible standard library features.