Hyperapp Brings the Best of the Elm Architecture into a Front-end Framework
Companion demo: Repository | Live app
Hyperapp is a super-minimalist UI framework. It’s highly declarative, borrowing the best parts of The Elm Architecture. Hyperapp has an extremely small footprint, weighing in at 4.3 kB minified or 1.9 kB minified+gzipped, which can be optimized further with tree-shaking. And yet, it’s quite feature-rich and allows you to write apps in a purely functional way.
Hyperapp brings a custom virtual DOM implementation and the recent from-the-ground-up v2 rewrite bakes the action “dispatch” layer right into the framework rendering layer. This results in some amazing conveniences. For example, built-in handlers can directly accept functions from “state” to “newly updated state”:
<button onclick={state => state + 1}>Increment</button>
Note: As of this writing, Hyperapp v2 is in beta, and its docs are sometimes incomplete. Not to worry: The Hyperapp team is constantly improving them, so it’s just a matter of time. You might also be interested in v1—many interesting projects have been built with it, after all.
Outstanding Features
- Purely functional approach makes it extremely clear and easy to reason about dataflow.
- Managed side-effects and effects-as-data approach à la Elm and Cycle.js—this is huge! If you use React, you’ll wish you had this too.
- No app code boilerplate: It concentrates on what to do instead of how.
- Highly testable: a consequence of your entire app being a pure function from state with effects isolated.
Biggest Drawback
While Hyperapp code is amazingly easy to reason about, there is one caveat: There’s no clear notion of a “component”. Hyperapp avoids being “yet another component-based UI framework”—cool in some ways, but using components as UI composition units would be handy for reusability, including its own local state and logic. (On the other hand, its functional composition is wonderful.)
Note that the Hyperapp team encourages developers to use Web Components’ Custom Element API to define native-like components - they’re the easiest way to run arbitrary code outside of the Hyperapp scope.
Show Me the Code!
Let’s look at the official “counter app” example. hyperapp
brings in its own h
function to assist in creating virtual DOM nodes, but with some Babel (or TypeScript) magic, it’s easy (and cleaner) to use JSX:
import { h, app } from 'hyperapp';
app({
init: () => 0,
view: state => (
<div>
<h1>{state}</h1>
<button onclick={state => state - 1}>-</button>
<button onclick={state => state + 1}>+</button>
</div>
),
node: document.getElementById('app')
});
Pure Functions
All the actions in Hyperapp are defined as pure functions that return either state or state with effects. This means all event handlers like onclick
expect “reducers” to be passed, which will then update the state maintained by the framework. Reducers can be functions:
const reset = () => 0;
const decrement = state => state - 1;
const increment = state => state + 1;
app({
init: reset,
view: state => (
<div>
<h1>{state}</h1>
<button onclick={reset}>Reset</button>
<button onclick={decrement} disabled={state === 0}> - </button>
<button onclick={increment}>+</button>
</div>
),
node: document.getElementById('app')
});
You can then move reducers to an external module, reuse them elsewhere, and test them in isolation!
Single Best Feature: Side Effects as Data
Effects are simple functions and are not included in the core of Hyperapp. The hyperapp-fx
package contains a bunch of effects of all sorts and can be used in a Hyperapp app seamlessly today. In the near future, a whole bunch of official packages will be available, such as @hyperapp/time
, @hyperapp/events
and more. It’s also very easy (and fun!) to write your own.
One of Hyperapp’s most powerful patterns from The Elm Architecture is representing effects as data which the underlying framework can then handle. So, to perform some action with side effects—like making an HTTP call or writing to/reading from localStorage
—the app doesn’t perform the effect. It instead passes the description of the effect to the Hyperapp framework. (This is like Cycle.js, where effects are handled by drivers rather than app code.)
Here’s an example of an HTTP call to get a random quote:
import { h, app } from 'hyperapp';
import { Http } from 'hyperapp-fx';
const getQuote = () => [
'...', // the "loading quote" placeholder
Http({
url: 'https://quotesondesign.com/wp-json/posts?filter[orderby]=rand&filter[posts_per_page]=1',
action: (_, [{ content }]) => content
})
];
app({
init: 'Click here for quotes',
view: quote => <h1 onclick={getQuote}>{quote}</h1>,
node: document.getElementById('app')
});
The Http
function here doesn’t make an actual HTTP call: It instead issues an instruction to the framework to do so, represented as pure data.
This is incredibly easy to test: Tests can assert against pure data being passed into the app and pure data describing the new state and effects to run.
Even things like setting focus are pure data in the application. For example, in the following snippet that sets the focus to an <input>
element, the setFocus
reducer is still all pure. The focus
function from @hyperapp/dom
only issues the “command” for the underlying framework to find an element with a certain id
and perform the DOM operation:
import { h, app } from 'hyperapp';
import { focus } from '@hyperapp/dom';
const setFocus = (state, id) => [state, focus({ id })];
app({
view: () => (
<div>
<input id="input" type="text" />
<button onclick={[setFocus, 'input']}>Set Focus</button>
</div>
),
node: document.getElementById('app')
});
Also, notice a nice tuple syntax here for passing additional payload to action: onclick={[action, payload]}
. Neat!
A Close Second: Subscriptions
Another magnificent Elm Architecture feature, Hyperapp’s subscriptions let the app listen for input from the outside world. The twist? Received events are—yet again—plain data on the app side. This is true even if it might have been generated by some impure code as the result of a side effect.
Subscriptions are also set up in a completely declarative way. Let’s say we want to update the app state’s time
property with a new time value every second:
import { h, app } from 'hyperapp';
import { interval } from '@hyperapp/time';
const tick = (state, time) => ({ ...state, time });
app({
subscriptions: state => [
interval(tick, {
delay: 1000
})
]
});
How Does It Compare to Mainstream Frameworks?
I don’t think we need to compare them: Hyperapp implies a different approach, not necessarily better or worse than that of a React, Angular, or Vue.js. The point is that it does bring its own major benefits. Hyperapp is darn good to make yourself familiar with because both it and The Elm Architecture highlight so many elegant state management and effect-handling patterns. Declarative side-effects are, in particular, something I wish to become more prevalent in mainstream frameworks over time.
What Is Hyperapp Best For?
This is a generic framework for web interfaces and could be used instead of, e.g., React. It boils down to preference, really. The difference mostly lies in an imposed programming style, heavily favoring functional and declarative approaches. I wouldn’t consider it a “niche” one, either—it just works so well, and the density of useful patterns per LOC is super high. Definitely give it a try!