Pure programming with Hyper App

When every function is pure, advanced async actions are easy.

Trying Hyper App

I am constantly looking at new libraries and frameworks (helps me be a better hacker). Recently I saw an interesting Elm-like tiny framework for making web apps called HyperApp. The syntax looks great, and it is compatible with ES6 template strings, making it easy to try right from NPM + CDN.

Here is an entire static HTML page with scripts to run HyperApp, including JSX-like ES6 template string view function.

1
2
3
4
5
<body>
<script src="https://unpkg.com/hyperapp"></script>
<script src="https://wzrd.in/standalone/hyperx"></script>
<script src="app.js"></script>
</body>

By default, HyperApp will attach itself to the document's body element, or you can specify the root of the application. Makes it incredibly simple to add HyperApp component to an existing MVC or static page!

The small program to increment the page is below. It has a single counter value we can increment by clicking the button.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
const html = hyperx(hyperapp.h)
const model = 0
const click = model => model + 1
const actions = {click}
const view = (model, actions) => {
return html`
<div>
<h1>Clicked ${model}</h1>
<button onClick=${actions.click}>click</button>
</div>
`

}
hyperapp.app({
model,
actions,
view
})

You can see the result below (thanks to Zeit Now deployment)

I love the fact that I can simply <button onClick=${actions.click}>click</button> and not the usual <button onClick={() => actions.click()}>click</button> like React. Also, debugging just works because there is no event wrapping. But I digress.

Besides convenient HTML output (via ES6 string templates without any transpiling), there is another huge advantage to this framework. Notice that the view function only uses its arguments to compute and return the result. The second argument actions is NOT the same object const actions = {click} we have constructed before calling hyperapp(...). One can check like this

1
2
3
4
5
6
7
8
9
10
11
12
const actions = {click}
const view = (M, A) => {
console.log('actions === A?', actions === A)
return html`
<div>
<h1>Clicked ${M}</h1>
<button onClick=${A.click}>click</button>
</div>
`

}
// prints on click
// "actions === A? false"

Similarly, click function only operates on its arguments (that has data and actions object). We could have written something like

1
2
3
4
5
6
7
8
9
10
11
12
const click = model => model + 1
const doubleClick = (model, data, actions) => actions.click(actions.click(model))
const actions = {click, doubleClick}
const view = (M, A) => {
return html`
<div>
<h1>Clicked ${M}</h1>
<button onClick=${A.click}>click</button>
<button onClick=${A.doubleClick}>click x 2</button>
</div>
`

}

And this just works, as you can see below

Protip you can use DevTools on this post's page and debug the source for the above iframe! Works like a charm, for example placing the breakpoint inside the view function allows you to look around my example code.

Peek into HyperApp example inside the iframe

Sweet!

Delaying actions

If "click" and "doubleClick" are pure functions operating on the model, aren't they like reducers in Redux pattern? Yes, but under the hood HyperApp wraps these functions to call view again. We can see that A.click is a very different beast from const click = model => model + 1 we passed in.

1
2
3
4
5
6
const view = (M, A) => {
console.log(A.click)
...
}
// prints
// A.click function (e){for(f=0;f<.onAction.length;f++)...,r(h,m)}

The code that wraps user-supplied actions with HyperApp code is here. In essence it calls a user action. If it returns a promise HyperApp waits, in other cases it renders the view with updated model.

1
2
3
4
5
6
7
8
9
10
11
// "wrap" is created by HyperApp and passed around inside "actions" object
wrap = function (data) {
// "action" is user supplied function
var result = action(model, data, actions, onError)
if (result === undefined || typeof result.then === "function") {
return result
} else {
model = merge(model, result)
render(model, view)
}
}

This means a couple of interesting things. One of them is that we can delay actions right away by delaying calling the wrapper function. Calling the wrapper function will trigger the view update - no need to separately call anything (like calling for digest cycle in Angular 1)

1
2
3
4
5
6
7
8
9
10
11
12
const actions = {
click: model => model + 1
}
const view = (M, A) => {
const delay = () => setTimeout(A.click, 1000)
return html`
<div>
<h1>Clicked ${M}</h1>
<button onClick=${delay}>click</button>
</div>
`

}

Clicking on the button above only updates the page after 1 second delay.

Quick aside HyperApp supports promise-returning and async actions, thus you can write something like this when updating the model

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
const delay = time =>
new Promise((resolve, reject) => setTimeout(() => resolve(), time))
const actions = {
add: model => model + 1,
async slowlyAdd(model, _, actions) {
await delay(1000)
actions.add()
}
}
const view = (model, actions) => {
return html`
<div>
<h1>Clicked ${model}</h1>
<button onClick=${actions.slowlyAdd}>click</button>
</div>
`

}

See delay demo and read actions API

Debouncing actions

The example above has a flaw - if we click the button multiple times, the counter will be increments quickly after 1 second. The actions are NOT debounced, they are just delayed. Let us add a little bit of polish by "queuing up" actions to update the counter at most once per second.

I will use Lodash debounce to make sure multiple calls are not causing an avalanche of increments.

1
2
3
4
5
6
7
8
9
const view = (model, actions) => {
const debounced = _.debounce(actions.click, 1000)
return html`
<div>
<h1>Clicked ${model}</h1>
<button onClick=${debounced}>click</button>
</div>
`

}

Good, now only a single increment happens a second later, even I quickly double clicked the button.

Reactive stream

Yet the example above drops clicks, instead of queuing them up. Here is where we can use a little bit of reactive streams magic. I have added Rx v5 to the page to get Rx observables and operators. I also included console-log-div for simple messages.

The view function looks the same, but I have renamed the action that handles the "click".

1
2
3
4
5
6
7
8
const view = (model, actions) => {
return html`
<div>
<h1>Clicked ${model}</h1>
<button onClick=${actions.scheduleClick}>click</button>
</div>
`

}

The interesting code is in our model. We will have a single stream, and we will schedule action functions using Rx.concatMap operator. BTW the docs for concatMap are not great. A simpler explanation: it flattens all streams into a single stack. For example, several streams arrive close in time. Each stream is 1 second long from start till end.

1
2
3
4
5
6
7
8
start --------------------->
new streams, each has value and end event
x---|>
y===|>
z~~~|>
start.concatMap(x, y, z) produces
(using different symbols for clarity)
----x---y===z~~~---->

Here is the reactive code that makes and queues clicks up using .concatMap

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
const delay$ = _ => Rx.Observable.of(_).delay(1000)
const delayedClicks$ = (new Rx.Subject())
.concatMap(delay$)
const actions = {
add: model => model + 1,
scheduleClick (model, _, actions) {
delayedClicks$.next(actions.add)
}
}
const started = performance.now()
delayedClicks$.subscribe(add => {
const time = performance.now() - started
console.log(`adding at ${time}ms`)
add()
})

When a user clicks on a button, a new event is pushed into delayedClicks$ stream. It has to wait for a second (or for previous events to complete). Then the event subscriber function runs, printing the time and calling the add action wrapped function. The interesting thing here is that due to app's pure actions, we have been able to pass them through the stream!

1
2
3
4
5
6
7
8
// the click pushes "add" function into the stream
scheduleClick (model, _, actions) {
delayedClicks$.next(actions.add)
}
// the observer who is subscribed to the stream gets "add" function
delayedClicks$.subscribe(add => {
add()
})

How cool is that?! Instead of "flagging" time information and storing it in the state, we simply let the Rx stream take care of "delaying" functions and then call them when needed. This approach is similar to action streams shown by Christian Alfoni Jørgensen in CycleJS driven by state and by Jay Phelps in the video RxJS + Redux + React = Amazing! (but the later only passes Redux "dumb" serializable action objects through the stream, not the reducer functions).

Conclusions

It was really simple to start using HyperApp, and to push it a little bit. I kind of liked the wrapped action functions that "knew" how to do rendering when they were done.