A Fresh Take on Modular Hyperapp
Tags: hyperapp, javascript, webdev
Published: 2021-12-10 11:00:19


Back in the summer of 2020 I wrote [an article series](https://dev.to/zaceno/modular-hyperapp-part-1-7f6) on how to make your Hyperapp-apps modular. The ideas & concepts discussed are still valid, but the method was cumbersome and my explanation dense. I've since refined how I componentize apps, so it's time for an update!



## Domains of the Counter Game

Here's a game. It goes like this. Click the plus and minus buttons to increase and decrease a value. When it reaches ten the game is over. You score one point for each button-click. Give it a try!

{% codepen https://codepen.io/zaceno/pen/YzrqaEm %}

Yes, it's the dumbest game ever made. But it's a simple example of an app having some distinct _domains_ – parts that make sense to think about in isolation from the rest.

Take the counter for example. You could imagine replacing the counter for a different (more interesting!) game while the scoring system, and the flow of screens from "play a game?" to "game over", could remain the same. 

Implementations designed to make removing/replacing domains easy tend to be very maintainable. So let's see how maintainable the implementation is.

## Domains have their own State

Have a look in the `JS` tab above. We find that replacing is the counter for something else isn't exactly easy, because all the domains and their interactions are tangled up in a compact set of actions.

What we want is that actions belonging to a certain domain, should only affect the state of that domain. Take the `Decr` action for example: 

```js
const Decr = (state) =>
  state.mode !== "play"
    ? state
    : {
        ...state,
        counter: state.counter - 1,
        score: state.score + 1
      };
```

`Decr` is for decrementing the counter, so it belongs to the counter domain. It should only affect the counter state:

```js
const Decr = state => ({ ...state, counter: state.counter - 1 })
```

But then what about scoring points? That belongs to the score domain, so there should be a separate action for that: 

```js
const ScorePoint = state => ({ ...state, score: state.score + 1 })
``` 

But `Decr` still needs to make `ScorePoint` _happen_. To do that, we add an in-line effect in `Decr` which _dispatches_ `ScorePoint`:

```js
const Decr = state => [
  { ...state, counter: state.counter - 1 },
  dispatch => dispatch(ScorePoint)
]
```

## Effects for dispatching other actions

Using Hyperapp's effect system this way, to only dispatch another action, might seem like a hack (and maybe it is?). I think it makes sense, and here's why:

Imagine your app logic as a circuit board. There are points where you connect inputs such as sensors, buttons, et.c. Pressing a button connected to a certain point, is analogous to dispatching a certain action. Also, there are points where you send outgoing signals to activate whatever is connected – analogous to effects.

![single circuit board](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/y6piwpfuq0oyh53ldm7n.png)

Let's say that instead of building your own circuit board from scratch, you source several smaller circuit-boards that do the various things you need, and hook them up. That means some output-connectors (effects) will need to signal (dispatch) some input connectors (actions) on other boards.

![split up circuit board](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/fs4o1fzs88b0p71j2j6i.png)

## Dividing actions by Domains

Let's keep doing this to untangle the actions from each other. 

The `Incr` action of the counter can be treated in the same way we changed `Decr`, but also we need to end the game once the value becomes 10:

```js
const Incr = state => [
  { ...state, counter: state.counter + 1 },
  dispatch => dispatch(ScorePoint),
  state.counter === 9 && (dispatch => dispatch(EndGame)),
]
```

Of course we need to implement the `EndGame` action, for affecting the mode state – another domain:

```js
const EndGame = state => ({ ...state, mode: 'finish' })
```

The `Play` action:

```js
const Play = state => ({
  mode: "play",
  counter: 0,
  score: 0
})
```

... also belongs to the mode domain. It represents the game starting, so it also needs to make sure to initialize the score and counter:

```js
const Play = state => [
  {...state, mode: 'play'},
  dispatch => {
    dispatch(InitScore)
    dispatch(InitCounter)
  }
]
```

And now those actions need to be defined as well.

```js
const InitScore = state => ({...state, score: 0})
const InitCounter = state => ({...state, counter: 0})
```

Now each of the three domains – mode, score and counter – each have a set of actions for managing their domain's state with full sovereignty.

### A Counter Component

Our goal is to be able to change stuff in one domain, without breaking anything outside it. So let's start with the counter an bundle everything that belongs there separately from the rest:

 ```js
const Counter = () => {
  const Init = state => ({ ...state, counter: 0 })

  const Decr = state => [
    { ...state, counter: state.counter - 1 },
    dispatch => dispatch(ScorePoint)
  ]

  const Incr = state => [
    { ...state, counter: state.counter + 1 },
    dispatch => dispatch(ScorePoint),
    state.counter === 9 && (dispatch => dispatch(EndGame)),
  ]

  return {Init, Incr, Decr}
}
```

There is also this part from the view:

```html
<div>
  <h1>${state.counter}</h1>
  <button onclick=${Decr}>-</button>
  <button onclick=${Incr}>+</button>
</div>
```

Let's put it in the component as well. 

```js
const Counter = () => {
  
  //...
  
  const view = state => html`
    <div>
      <h1>${state.counter}</h1>
      <button onclick=${Decr}>-</button>
      <button onclick=${Incr}>+</button>
    </div>`

  return {Init, view}
}
```

Now for the app to use this component, we need to instantiate it :

```js
const counter = Counter()
```

(Why though? – We'll get to that in a sec)

In the `Play` action we replace `InitCounter` with `counter.Init`, and in the view we replace the counter-html with: `${counter.view(state)}`

This way everything related to both behavior and appearance of a counter is defined in one place. As long as we return the same interface (`{Init, view}`) we can change whatever we want about the counter without affecting the rest of the app.

_However_, that same assurance doesn't hold in the other direction! This component is dependent on keeping its state in `state.counter`. Also on the `EndGame` and `ScorePoint` actions being available in the scope.

## A _Reusable_ Counter Component

Instead of relying on certain external facts to be true, the necessary information should be provided to the component from whoever consumes. 

We will need to be given a `get` function that can extract the counter state from the full app state. 

We will also need a `set` function that can produce a new full app state given the current full state and a new counter state. 

Also, we need an `onChange` action we can dispatch when the value changes. That way it can be up to the consumer wether to score a point, end the game or do something else entirely.

Adapting the counter component to these changes, it looks like:

```js
const Counter = ({get, set, onChange}) => {
  const Init = state => set(state, 0)

  const Decr = state => [
    set(state, get(state) - 1),
    dispatch => dispatch(onChange, get(state) - 1)
  ]

  const Incr = state => [
    set(state, get(state) + 1),
    dispatch => dispatch(onChange, get(state) + 1)
  ]

  const view = state => html`
    <div>
      <h1>${get(state}</h1>
      <button onclick=${Decr}>-</button>
      <button onclick=${Incr}>+</button>
    </div>`

  return { Init, view }
}
```

Instantiating the component now looks like:

```js
const counter = Counter({
  get: state => state.counter,
  set: (state, counter) => ({...state, counter}),
  onChange: (state, value) => [
    state,
    dispatch => dispatch(ScorePoint),
    value === 10 && (dispatch => dispatch(EndGame))
  ]
})    
```

Since everything the counter needs to know about the outside world is provided in this instantiation, it is no longer sensitive to changes outside of it. Moreover, we can easily have multiple counters in the same app, for different purposes without implementing them separately. We just instantiate the counter component multiple times for different states. In other words, this component is _reusable_!

### Composing App Components

I started calling this thing a 'component' because it is _composable_. Several components like this could be combined together to define our app. 

Rather than walk you through how to componentize the other domains, here's the same fun game again – this time with different domains componentized and composed to define the app:

{% codepen https://codepen.io/zaceno/pen/XWedEbV %}

Especially notice how the counter is instantiated as a sub-component of game. Also how the game's _two_ views are passed as arguments to the _flow_ component. 

There's nothing remarkable about this structure in particular – it could be done in a myriad of ways. This one just made sense to me. 

> If something is unclear please drop a question in the comment section!


### Final Thoughts

So, am I suggesting you go refactor your entire app now? No, definitely not. I made the game fully componentized just for illustrative purposes. As you can see it can get a bit boilerplaty and besides, it's not always so clear how to draw the line between domains.

So when should you use this approach? The main win is the separation that makes it safe to work on one thing without accidentally breaking something else. So if you have some especially tricky logic that you don't want getting in the way of your other work, you could tuck it away in a component. Another example might be if your app has several different pages with different things going on in each, you could make it easier for a team to work on different pages in parallell without merge-conflicts. Also: reusability is a big win. If you have multiple instances of the same behavior, you _want_ to reuse it one way or another.

If you find it useful, I'd love to hear about it!

_Special thanks to @mdkq on the Hyperapp Discord, for reminding me I needed to publish this, and also inspiring me to reconsider some things i had dismissed earlier._