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 | <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 | const html = hyperx(hyperapp.h) |
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 | const actions = {click} |
Similarly, click
function only operates on its arguments (that has data
and actions object). We could have written something like
1 | const click = model => model + 1 |
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.
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 | const view = (M, A) => { |
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 | // "wrap" is created by HyperApp and passed around inside "actions" object |
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 | const actions = { |
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 | const delay = time => |
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 | const view = (model, actions) => { |
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 | const view = (model, actions) => { |
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 | start ---------------------> |
Here is the reactive code that makes and queues clicks up using .concatMap
1 | const delay$ = _ => Rx.Observable.of(_).delay(1000) |
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 | // the click pushes "add" function into the stream |
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.