A Walk through Hyperapp 2

Zacharias Enochsson
Hyperapp
17 min readAug 25, 2019

--

In this article, I invite you to follow me as we build a simple timer app. Along the way I’ll introduce you to all of the major features of Hyperapp 2. Even if you’ve worked with Hyperapp before, version 2 has enough changes that you’ll likely find this illuminating.

I’ll be using several modern JavaScript idioms, in particular arrow-functions, destructuring, and rest/spread operators. If you’re not familiar with these, you might want to look into them first.

Since our focus will be on how Hyperapp works, we won’t be setting up a project structure or configuring a bundler. To follow along, just fire up your favorite editor and create this HTML file:

<!DOCTYPE html>
<html lang="en">
<head>
<script type="module">
import { h, app } from "https://unpkg.com/hyperapp@2.0.3"

/* Your code goes here: */
</script>
</head>
<body>
<main id="app"></main>
</body>
</html>

… then load it up in your browser. Now, let’s make an app!

State Machines

Actually… Let’s first talk about state machines — a simple yet powerful way to describe and reason about any kind of system. Designing our app in terms of a state machine will translate directly to much of the implementation since Hyperapp’s architecture is rooted in this concept.

Picture a box. The machine we want to describe is a system inside that box. It interacts with the world outside through inputs and outputs. The inputs are the various ways our machine reacts to the outside world. The outputs are how it affects the outside world.

A State machine has inputs and outputs, with a memory in-between.

The peculiar thing about state machines is that there isn’t necessarily a one-to-one mapping from inputs to outputs. The machine has a memory — a changing internal configuration called the state.

The state will change in a certain way for any given input. The new state is determined purely as a function of the input and the current state.

Outputs are actuated in response to an input. Which output — if any — is also a function of the input and current state. These relationships entirely define the state machine.

Actions

Every Hyperapp-app is a state-machine, whose inputs are called actions. In code, actions are expressed as pure functions that take the current state as their first argument and return what the new state should be.

Our goal is to build a simple timer, so let’s specify the various possible states it can have, as well as the actions that will take us from one state to another.

Initially, our timer is stopped — that’s one state. When the timer is stopped, we can start it — that’s an action. Once started, the timer is running — another state…

The states we’ll need are: “stopped”, “running” and “paused”. The actions to transition between them are:

  • Start : if the state is stopped, the new state should be running. Otherwise: no change.
  • Pause : if the state is running, the new state should be paused. Otherwise: no change.
  • Continue: if the state is paused, the new state should be running, Otherwise: no change.
  • Cancel : regardless of the current state, the new state should be stopped.

Or, in code:

const Start = state => state === "stopped" ? "running" : stateconst Pause = state => state === "running" ? "paused" : stateconst Continue = state => state === "paused" ? "running" : stateconst Cancel = state => "stopped"

While the state can be as simple as a string or a number, any realistic app will need to keep track of more than one variable. Further down, for example, we’ll need to remember not only that the timer was started, but when it was started. To accommodate this we’ll make the state an Object. That means our actions will have to look like this instead:

const Start = state =>
state.mode === "stopped"
? { ...state, mode: "running" }
: state
const Pause = state =>
state.mode === "running"
? { ...state, mode: "paused" }
: state
const Continue = state =>
state.mode === "paused"
? { ...state, mode: "running" }
: state
const Cancel = state => ({ ...state, mode: "stopped" })

Initialization

But those functions are just inert logic, sitting there waiting to be used. We need to instantiate a machine to use them. That is what the app function (exported by Hyperapp) does. Add this to the bottom of your code.

app({
init: { mode: "stopped" }
})

Notice how we give it an initial state with theinit property.

This runs without errors but doesn’t do anything either because we haven’t defined what our app should look like. We still need to define a view.

The View

Remember how state machines have “outputs” through which they affect the world around them? Hyperapp has one especially useful output built-in which is actuated on every state change. It is the view and it updates the DOM so that it reflects the current state according to your design.

The view is a function you define. It takes the current state as an argument and returns a tree of virtual DOM nodes — i.e. a lightweight description of what the actual DOM on our page should look like.

Every time the state changes, our view is called, so that we can tell Hyperapp how we’d like the DOM to look, given the new state. Hyperapp then takes care of actually modifying the DOM to match.

Virtual DOM nodes are created using the h function. Add a view to the app-definition like this:

app({
init: { mode: "stopped" },
view: state => h("div", {}, [
h("p", {}, ["Current state: ", state.mode])
]),
node: document.getElementById("app")
})

Notice that we also need to tell Hyperapp where on the page to attach the view, via the node property.

Load the page and you’ll see: “Current state: stopped”.

Reacting to DOM Events

We have something visual. But we still need buttons to dispatch our actions, in order to provide an interactive app. Change the view to this:

state => h("div", {}, [
h("p", {}, [

state.mode === "stopped"
? h("button", { onclick: Start }, "START")
: h("button", { onclick: Cancel }, "CANCEL"),

state.mode === "paused"
? h("button", { onclick: Continue }, "CONTINUE")
: h(
"button",
{
disabled: state.mode === "stopped",
onclick: Pause
},
"PAUSE"
)
]),
h("p", {}, ["Current state: ", state.mode])
])

There are a couple of things going on here, so take a moment to study the code, and try it out.

First, notice that clicking buttons now dispatch actions, causing the state to update accordingly. This is because we assigned an action to each button onclick event handler.

Furthermore, notice how we swap the START button for CANCEL when the mode is “running”. And how the PAUSE button is disabled when the mode is “stopped” (because pausing doesn’t make sense when it’s already stopped).

This is how Hyperapp closes the UI loop, providing an interactive interface from where a user can dispatch actions, which modify the state, which updates the interface, and so on…

Here’s the full, runnable example so far, in case you got lost along the way:

This is starting to look useful. But without the crucial aspect of time, we can hardly call it a timer.

Accessing Data from DOM Events

A state machine by definition has no concept of time. Whatever data it tracks, manages or mangles must be fed to it as inputs, including time.

Originally, state machine diagrams were used to model simple digital circuits with a finite number of on/off inputs. That approach would mean we’d have to have three states multiplied by all the possible times — terribly impractical of course! Instead, we simply allow each input to be accompanied by value (the “payload”).

So, does Hyperapp provide the time as a payload to our actions? — Not quite, but almost. For any action dispatched in response to a DOM event, Hyperapp will provide the event object as the second argument to the action. The event object has a timeStamp telling us when the event occurred.

So, in order to keep track of time, change the actions as follows:

const DURATION = 15000 //ms = 15sconst Start = (state, event) =>
state.mode === "stopped"
? {
mode: "running",
startedTime: event.timeStamp,
remainingTime: DURATION,
duration: DURATION
}
: state
const Pause = state =>
state.mode === "running"
? {
...state,
mode: "paused"
}
: state
const Continue = (state, event) =>
state.mode === "paused"
? {
...state,
mode: "running",
startedTime: event.timeStamp,
duration: state.remainingTime
}
: state
const Cancel = state => ({
...state,
mode: "stopped"
})

Also, add:

...
state.remainingTime
&& h("p", {}, ["Remaining: ", state.remainingTime, " ms"]),
...

…to the view, and check your app. When you click the “START” button, you’ll see: “Remaining: 15000ms”.

But nothing more happens after that. From the diagram above, it is clear why: we haven’t added the “Update” action yet. There is nothing to tell our app that time is moving forward.

Subscriptions

The action itself could look like this:

const UpdateTime = (state, timestamp) =>
state.mode !== "running" ? state
: state.remainingTime < 0 ? Cancel(state)
: {
...state,
remainingTime:
state.duration + state.startedTime - timestamp
}

But how do we get UpdateTime dispatched? Clicking a button like in our previous actions is no good. We want it dispatched continually and frequently, with a requestAnimationFrame loop.

In the real world of web app development, it is common to need to react to events from sources other than users interacting with the DOM. For all those situations, Hyperapp dictates the use of subscriptions.

Add this line to your code (after the first import statement):

import { onAnimationFrame } from "https://unpkg.com/@hyperapp/events@0.0.3"

Voila! — You’ve just imported the animation frame subscription. To use it, add a subscriptions declaration to your app definition:

app({
...
subscriptions: state => [
state.mode === "running" && onAnimationFrame(UpdateTime)
]
})

You can think of the app’s subscriptions as conceptually similar to the view. It’s a function that is called each time the state changes. It should return an array with all the subscriptions you want your app to listen to. Hyperapp will start listening, each time a new subscription appears in the array, and stop listening whenever it disappears. You don’t need to keep track of starting/stopping listeners yourself.

With the code above, whenever state.mode changes to “running”, Hyperapp will start the subscription to onAnimationFrame with the UpdateTime callback. And when the timer should no longer be running, the subscription will be shut off.

Here’s what we’ve got so far:

Implementing your own Subscriptions

The timer works. The next step is making it prettier. But let’s put that on hold for a deeper look at subscriptions.

If you’re like me, you’re a little bothered that we had to import something else in order to to use requestAnimationFrame — something one might have expected to just use natively. Subscriptions are great because they alleviate the hassle of managing listener references. The downside is that anything you want to subscribe must be wrapped in a unified interface that Hyperapp can understand.

At the time of writing, Hyperapp offers subscriptions for some of the most common event sources such as mouse-/keyboard-events, setInterval, and animation-frames. Others, such as web-sockets, firebase, etc, you’ll need to wrap yourself in order to be able to subscribe to them.

Fortunately, such a wrapper is very thin and easy to make.

First, the function onAnimationFrame(someAction) is a simple function which returns a length-2 array (tuple):[subscriptionFunction, {action: someAction}]. The first element is thesubscriptionFunction where we define how to actually start and stop listening. The second element is an object containing all the options for the subscription.

Hyperapp knows whether to start/stop or restart a subscription by detecting whether the subscriptionFunction is new or removed in the subscriptions-array, or if any of the options have changed. When Hyperapp wants to start a subscription, it calls the subscription function with two arguments. The first is dispatch — a function the subscription can use to dispatch actions in response to events. The second argument is the options-object.

The subscription function takes care of starting the subscription and should return a function for stopping it.

Code is sometimes clearer than words, so here’s how onAnimationFrame is actually implemented (modified for clarity):

const onAnimationFrame = (() => {
const subFn = (dispatch, options) => {
let id = requestAnimationFrame(function frame(timestamp) {
id = requestAnimationFrame(frame)
dispatch(options.action, timestamp)
})
return () => cancelAnimationFrame(id)
}
return action => [subFn, { action }]
})()

HTML Attributes & Styling

We’ve done what we set out to do: we’ve made a working timer. It doesn’t look so great though — we should give it a bit more flair! Replace the print outs of state.mode and state.remainingTime in your view, with this:

h("div", { class: "gauge" }, [
h("div", {
class: "gauge-meter",
style: {
width:
state.mode !== "stopped"
? (100 * state.remainingTime) / DURATION + "%"
: "100%"
}
}),
state.mode !== "stopped" &&
h("p", { class: "gauge-text" }, [
Math.ceil(state.remainingTime / 1000),
" s"
])
])

What we’re doing here is creating two divs inside each other and a p with the remaining time in seconds. The goal is to make a progress-bar-like visualization of the remaining time. We want to apply some CSS to give the inner div a nice bright color, and the outer one, a dull color.

Notice how we set thestyle.width of the inner div to a percentage of the outer div’s width, corresponding to the remaining time of the duration. Also note how we set class attributes for the virtual dom nodes, in order to apply CSS from a stylesheet. Go ahead and add the CSS yourself, or copy & paste from the example below.

Reusable Views

What could possibly top this glorious timer? — Why, two timers of course!

The great thing about defining our view through a bunch of nested h function-calls is that if there’s a part we’d like to use repeatedly, we can simply bundle that up in its own function, and call that function as many times as we’d like in the view.

Move the part of the view that describes the gauge into its own function:

const Gauge = state => h("div", { class: "gauge" }, [

h("div", {
class: "gauge-meter",
style: {
width:
state.mode !== "stopped"
? (100 * state.remainingTime) / DURATION + "%"
: "100%"
}
}),

state.mode !== "stopped" &&
h("p", { class: "gauge-text" }, [
Math.ceil(state.remainingTime / 1000),
" s"
])
])

Move the buttons to another function of their own:

const Controls = state => h("p", {}, [

state.mode === "stopped"
? h("button", { onclick: Start }, "START")
: h("button", { onclick: Cancel }, "CANCEL"),

state.mode === "paused"
? h("button", { onclick: Continue }, "CONTINUE")
: h(
"button",
{
disabled: state.mode === "stopped",
onclick: Pause
},
"PAUSE"
)
])

Now, having two timers in the view is simply a matter of calling those functions twice:

view: state => h("div", {}, [
h("p", {}, "timer 1:"),
Controls(state),
Gauge(state),
h("hr"),
h("p", {}, "timer 2:"),
Controls(state),
Gauge(state)
])

Give it a try and you’ll notice a problem: both timers are completely in sync! This is not really surprising as both timers are based on the same state. If we want two independently operating timers, each timer needs it’s own state.

In Hyperapp, all of the app’s state goes in one single place. If you have multiple elements which should behave the same, but independently, they will each require their own identical yet separate set of properties in the state:

app({
init: {
timer1: { mode: "stopped" },
timer2: { mode: "stopped" }
}
...
})

With this, we can change the view to pass separate sets of state to the different gauges & controls:

view: state => h("div", {}, [
h("p", {}, "timer 1:"),
Controls(state.timer1),
Gauge(state.timer1),
h("hr"),
h("p", {}, "timer 2:"),
Controls(state.timer2),
Gauge(state.timer2)
])

The buttons don’t work anymore though. That is because the actions still don’t know about the separate timer-states. We need to make the actions operate on the right timer, depending on where we clicked.

Parametrized Actions & Payload Filters

Fortunately, we don’t need to change any of the actions we made before. We can simply reuse them in new actions which know how to work on a particular timer, specified by the payload. Add this to your code:

const timerXAction = action => (state, { id, payload }) => ({
...state,
[id]: action(state[id], payload)
})
const StartX = timerXAction(Start)
const CancelX = timerXAction(Cancel)
const PauseX = timerXAction(Pause)
const ContinueX = timerXAction(Continue)
const UpdateTimeX = timerXAction(UpdateTime)

Now you’re probably wondering about that funny looking payload definition {id, payload}. That will be the next order of business.

We explained earlier, that when actions are dispatched because of a DOM-event, the second argument (the “payload”) will be the event object. That is just a default. Hyperapp allows you to specify any value you wish by passing it along with the action in a tuple, like this:

h("button", { onclick: [MyAction, "foo"] }, "click me")

When that button is clicked, MyAction will be dispatched, but the payload will not be the event object but rather the string "foo". This is useful when you want to dispatch an action from various contexts.

But what if you want the event object and a parameter? Hyperapp has another trick up its sleeve: if the parameter you provide is a function (a “payload filter”), then when the action is dispatched, the function will be called with the default payload (the event object) and use the return value as the payload for the action.

So, in order for our actions to know both the event and the timer-id in the payload (i.e. {id, payload} above) we need to define a payload creator like this:

const withId = id => payload => ({ id, payload })

Now that we have that, we can finish what we started by first passing the id of each timer along to each Controls in the view:

...
Controls(state.timer1, "timer1"),
...
Controls(state.timer2, "timer2"),

Update the Controls view to use this parameter in the actions, like this:

const Controls = (state, id) => h("p", {}, [

state.mode === "stopped"
? h("button", { onclick: [StartX, withId(id)] }, "START")
: h("button", { onclick: [CancelX, withId(id)] }, "CANCEL"),

state.mode === "paused"
? h("button", { onclick: [ContinueX, withId(id)] }, "CONTINUE")
: h(
"button",
{
disabled: state.mode === "stopped",
onclick: [PauseX, withId(id)]
},
"PAUSE"
)
])

Finally, we’ll need to update the subscriptions. Each timer will need its own subscription, like this:

subscriptions: state => [

state.timer1.mode === "running" &&
onAnimationFrame([UpdateTimeX, withId("timer1")]),

state.timer2.mode === "running" &&
onAnimationFrame([UpdateTimeX, withId("timer2")])
]

You should now have two separately working timers! If not, compare your code to this example:

The Single-state philosophy

If you’re familiar with frameworks like React that support self-contained components which manage their own state, you might be a little surprised by how hard it was to go from one to two independently working timers. You’re not wrong — it is harder in this case.

But consider this: the paradigm of self-contained, stateful components that React and others are based on is optimized for truly independent components — which is almost never the case.

More often than not, components depend on each other, demanding intercommunication. The interconnectedness can lead to complexity, which has driven the development of state-management tools like Redux, MobX et.c. — Tools that keep your app’s state in a central store separate from the view.

…just like Hyperapp! Hyperapp built-in state management gives you absolute control over your state, letting you tailor the distribution of state appropriately for your app, using only the most elementary of techniques: function composition.

Effects

We’ve spent a lot of time on making the app react to inputs, through subscriptions and the view. But what about affecting the outside world. What about the outputs of a state machine?

Technically speaking the view and subscriptions are outputs, but they come built-in and run on every state change. There are plenty of other ways an app might affect the outside world — HTTP-requests, play audio or simply good old window.alert(...). All of these are referred to as “effects” (because they model real-world side-effects).

By the definition of a state machine, outputs depend on input + current state. In Hyperapp, this means that if we want to produce an effect it should be returned from an action.

“What?! I thought actions are supposed to return only the new state!” — They are. When an action also needs to produce one or more effects, it should return a tuple like: [newState, anEffect(...), anotherEffect(...)].

Coming up next, we will hook up an effect to call window.alert("Timer 1 stopped") when timer 1 stops (and the same for timer 2 of course).

First, we need to change the UpdateTimeX action like this:

const UpdateTimeX = (state, { id, payload }) => {
const newState =
timerXAction(UpdateTime)(state, { id, payload })
return (
state[id].mode === "running"
&& newState[id].mode === "stopped"
? [newState, Alert(`${id} timed out`)]
: newState
)
}

Did you notice the Alert(...) in there? That’s the effect. Hyperapp doesn’t have an official effect for window.alert — at least not at the time of writing — so we’ll need to implement it ourselves. It’s a good thing we already talked about the anatomy of subscriptions since effects are implemented similarly. Simpler, even, since they don’t need to return a “stop-me” function.

The effect constructor Alert(...) should return a tuple of [effectFunction, options]. The effect function is called by Hyperapp, with dispatch and options as arguments. We won’t be needing dispatch in this case, but it’s useful for when you need to call back from effects such as HTTP requests.

We’ll implement ourAlert effect like this:

const Alert = (() => {
const effectFn = (dispatch, opts) => window.alert(opts.message)
return message => [effectFn, { message }]
})()

Add that to the code, and you should now get an alert each time you let one of the timers run out. If not, here’s the final example to check your code against:

In Conclusion

This concludes our walkthrough of Hyperapp 2. Let’s summarize what we’ve covered.

  • Hyperapp keeps an internal configuration for your app known as the state. The state changes when actions are dispatched.
  • Actions are just functions that take the current state as their first argument and return what the new state should be.
  • The view is a function you write, describing the DOM you want for a given state. Hyperapp will use it to update the DOM whenever the state changes.
  • The view can be defined to dispatch actions in response to DOM events. Actions are passed the DOM event object as a payload (second argument). You can customize the payload with additional information using parametrized actions and payload-filter functions.
  • Subscriptions are external event streams to which your app can react. You declare them as a function of the current state, via the subscriptions property. Hyperapp keeps track of live subscriptions for you, turning them off and on as designed. Subscriptions can be told, via their options, which actions to dispatch in response to events.
  • Actions may also cause effects in addition to changing the state. This is how your app makes things happen besides updating the DOM.

Believe it or not, that’s all there is to it! (Almost — We didn’t cover keys and Lazy components as they mainly pertain to optimization). With this tiny, simple framework you’re ready to make any browser-based app.

You probably still have plenty of questions, though: How do I do routing? Can I use JSX (yes, you can)? How do I integrate a third party widget? What if I really want stateful components? How do I structure a large complex project?

Hyperapp 2 is now in beta, with the official release coming out soon. That means best-practice patterns for common cases are still being discussed in the lively, helpful community on Slack and Spectrum. Come by and join the conversation!

I would like to say a special thank you to Jorge Bucaran, Mario Pizzinini & Vuong Nguyen for your valuable proof-reading and feedback. This is a much better article thanks to you!

--

--