Modular Hyperapp - Part 2

zaceno

Zacharias Enochsson

Posted on August 17, 2020

Modular Hyperapp - Part 2
NOTE: this article series is superceded by this article

In the first part of this series, we acknowledged that modules can be tricky. In particular, it's not easy to know from the start what modules you'll need down the road.

Thankfully, Hyperapp doesn't force you to think about modules up front. You simply start writing your app in the most straightforward way possible, with all the code in a single file.

It is only when that main file gets large and unwieldy that you need to start looking around for potential modules to break out. Usually you will start looking in the view, because that is the code that tends to grow the fastest.

View components

To give an example of breaking out a piece of view into a module, let's start with this app, which contains a counter:

import {app, h, text} from 'https://unpkg.com/hyperapp'

const Increment: state => ({
    ...state,
    counter: state.counter + 1
})

const Decrement: state => ({
    ...state,
    counter: state.counter - 1
})

app({
    init: {counter: 0},
    view: state => h('main', {}, [
        h('h1', {}, [text('My Counter:')]),
        h('p', {class: 'counter'}, [ 
            h('button', {onclick: Decrement}, [ text('-') ]),
            text(state),
            h('button', {onclick: Increment}, [ text('+') ]),
        ])
   ]),
   node: document.getElementById('app')   
})
Enter fullscreen mode Exit fullscreen mode

You wouldn't actually bother breaking out modules from an app this simple, but a realistic example would be harder to follow. Just pretend it is messier than it really is ;)

Since each node is defined by a call to h, breaking out just the counter is as easy as cut-n-pasteing the node you want into a new function:

const counterView = state => h('p', {class: 'counter'}, [
    h('button', {onclick: Decrement}, [ text('-') ]),
    text(state.counter),
    h('button', {onclick: Increment}, [ text('+') ]),
])
Enter fullscreen mode Exit fullscreen mode

I call functions like this "view components" – or "views" for short – because they are composable bits of view. (In the Hyperapp community they are often called "components")

This counterView allows us to express the main view in a more compact way:

state => h('main', {}, [
    h('h1', {}, [ text('My Counter:') ]),
    counterView(state),
])
Enter fullscreen mode Exit fullscreen mode

View components in modules

When we move counterView into a separate module (counter.js) it loses its references to Increment and Decrement, since they are still in the scope of the main module (main.js). There are three ways to fix that:

Option A: Move the actions to counter.js as well

The downside is that the actions are dependent on the full app state, so we would need to keep revisiting counter.js to update Increment and Decrement as we add new features.

Moreover we couldn't reuse this module for a second counter in the app, because it is bound specifically to these two actions.

Option B: Export the actions from main.js and import them in counter.js

This makes counter.js dependent on main.js, and has the same reusability issue as option A.

Option C: Pass the actions as arguments to counterView

This is the one we'll go with.

Speaking of which, we can't have counterView dependent on the full app state as an argument. It should expect the most concise and well defined set of arguments that provide just the values and actions it needs. It could look something like this:

// this is counter.js
import {h, text} from 'https://unpkg.com/hyperapp'

const view = ({value, Increment, Decrement}) =>
    h('p', {class: 'counter'}, [
        h('button', {onclick: Decrement}, [ text('-') ]),
        text(value), // <--- !!! not `state.counter`
        h('button', {onclick: Increment}, [ text('+') ]),
    ])

export {view}
Enter fullscreen mode Exit fullscreen mode

Models

With that, main.js becomes:

import {app, h, text} from 'https://unpkg.com/hyperapp'
import {view as counterView} from './counter.js'

const Increment = state => ({
    ...state,
    counter: state.counter + 1
})

const Decrement = state => ({
    ...state,
    counter: state.counter - 1
})

app({
    init: {counter: 0},
    view: state => h('main', {}, [
        h('h1', {}, [text('My Counter:')]),
        counterView({
            value: state.counter, // <-- 
            Increment,
            Decrement,
        })
   ]),
   node: document.getElementById('app')   
})
Enter fullscreen mode Exit fullscreen mode

The object {value, Increment, Decrement} is what I call the "model" for this particular view. It is up to the main view to map the current state and in-scope actions to the required model.

That's a bit more effort than just counterView(state) but modularization is about saving effort later at the cost of a bit more code now.

Reusability

With our counter in such a loosely coupled module, we can make it fancier with animated SVGs and what not, only by editing counter.js. More importantly, we can add a second counter with the same look but different behavior – without changing or duplicating counter.js!

import {app, h, text} from 'https://unpkg.com/hyperapp'
import {view as counterView} from './counter.js'

const IncrA: state => ({...state, A: state.A + 1})
const DecrA: state => ({...state, A: state.A - 1})
const IncrB: (state, x) => ({...state, B: state.B + x})
const DecrB: (state, x) => ({...state, B: state.B - x})

app({
    init: {A: 0, B: 0},
    view: state => h('main', {}, [
        h('h1', {}, [text('My Counter:')]),
        counterView({
            value: state.A,
            Increment: IncrA,
            Decrement: DecrA,
        }),
        h('h1', {}, [text('My Other Counter:')]),
        counterView({
            value: state.B,
            Increment: [IncrB, 3],
            Decrement: [DecrB, 3],
        }), 
   ]),
   node: document.getElementById('app')   
})
Enter fullscreen mode Exit fullscreen mode

Conclusion, Part 2

In summary: Manage your complex view by breaking it apart into view components, in separate modules. If those get too big, break them down further.

A view component takes a model as its argument, which is an object containing all of the values, as well as all of the actions it needs.

The most effective approach is to target repetitive chunks of view, as well as large chunks that can be hidden behind concise models.

That should keep your view code in check as your app continues to grow. Taming the other side of your app – the business logic – is what we'll focus on for the rest of the series.

💖 💪 🙅 🚩
zaceno
Zacharias Enochsson

Posted on August 17, 2020

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related

A Fresh Take on Modular Hyperapp
hyperapp A Fresh Take on Modular Hyperapp

December 10, 2021

Modular Hyperapp - Part 7
hyperapp Modular Hyperapp - Part 7

August 25, 2020

Modular Hyperapp - Part 6
hyperapp Modular Hyperapp - Part 6

August 24, 2020

Modular Hyperapp - Part 4
hyperapp Modular Hyperapp - Part 4

August 18, 2020