Modular Hyperapp - Part 2
Tags: hyperapp, javascript
Published: 2020-08-17 12:32:10

> ##### _NOTE: this article series is superceded by [this article](https://dev.to/zaceno/a-fresh-take-on-modular-hyperapp-4njm)_

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:

```js
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')   
})
```

> 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:


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

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:

```js
state => h('main', {}, [
    h('h1', {}, [ text('My Counter:') ]),
    counterView(state),
])
```

### 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:

```js
// 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}
```

### Models

With that, `main.js` becomes:

```js
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')   
})
```

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`!


```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')   
})
```

### 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.