Exploring Unidirectional Components in Mithril (part 1 — Hyperapp)

Vlad Sabev
7 min readAug 18, 2017

--

I’ve wanted to try writing UI components using a unidirectional data flow for a while — mostly as an exercise, but also with the hope that it will be easier to understand and reason about the data. And whether it would work out well or not, doing such an experiment would at the very least be a valuable learning experience.

But why wouldn’t we stick with our current approach anyway?

Everything is a class?

When using OOP as front end developers, our work often involves writing and managing a tangled mess of component classes, which we then turn into even more complex applications.

By sticking everything in a class, it becomes more difficult to reuse (or test) a single method from the outside — we have to create an instance of the whole class, including all these other methods and properties we don’t need.

Or, as Joe Armstrong, creator of Erlang, puts it:

You wanted a banana but what you got was a gorilla holding the banana and the entire jungle.

To make OOP harder to grasp in JavaScript specifically, we also have the mind-bending mystery that is this:

State mutation “considered harmful”

There are ways to tell whether state mutation is appropriate for our use case or not. There are also rules and principles we can apply to write object-oriented code correctly. And there are even tools and other tools to help with our this-related troubles.

But many of us silly geese fall into the same trap over and over again — we write convoluted methods that change a bunch of overlapping internal properties. And after calling a series of them several times, we’ve managed to lose track of our component’s state again. Or break it irrevocably.

Which, I’m assuming, is why sometimes the best way to get a computer to work is to restart it:

Ensuring mutable state integrity

So what can we do?

Presumably, if we treated our data as read-only and returned a new copy of it with the changes we want for every action, it would be easier to keep track of the state, unit test individual functions, use more functional programming approaches in our code, and even do fancy stuff like Hot Module Reloading / Time Travel Debugging.

Mithril — all folk desire it!

I’ve become fascinated with all the ultralight component-based frameworks that have been spinning off of React in the past couple of years. The simplicity and performance of Virtual DOM, combined with the significantly smaller size of these frameworks, has resulted in them becoming popular among front end developers, with many thousands of stars on GitHub and vibrant communities on Slack and Gitter.

Mithril has risen to the top of my development stack for mobile-ready web applications due to its 8 KB size, high performance, and integrated router. With a flexible approach to defining components, it makes a great candidate for exploring unidirectional data flow architectures.

The stateful quo

Let’s start with a typical approach to writing Mithril components. Obligatory implementation of a Counter component:

As far as class components go, it’s relatively straightforward — clicking the + button increments the counter, and clicking the - button decrements it until it reaches 0.

However, there is the issue of context that’s also present in React — when binding the onclick events of the buttons to the class methods, we have to call them with the correct this value.

If we put the decrement and increment methods on the class prototype, then onclick={this.increment} would result in the button’s DOM element being used as this instead of the class instance, and the counter’s value would not change!

To get the right context, we could bind event handlers by either using onclick={() => this.increment()}, onclick={this.increment.bind(this), or by defining the class methods as arrow functions, as we’ve done above. Neither of those is really developer-friendly, as it can sometimes trip beginners and experts alike.

Fortunately, Mithril also allows us to define components another way — meet the closure component:

In addition to avoiding potentially problematic references to this, the code is actually much shorter and clearer than the class definition.

Still, the developer directly manipulates the value of the count variable. In this case, it‘s not really an issue, since the component is quite simple, but for the sake of our experiment, let’s see where else we can get from here…

Enter Hyperapp

Hyperapp is another front end framework that pushes the size limit even lower than Mithril, weighing a measly 1.5 KB. Its adoption of The Elm Architecture is particularly interesting in that it encourages writing pure action functions, thus avoiding direct state mutations. In Hyperapp, our old friend — the Counter component — looks like this:

Here’s what’s going on:

  1. We first initialize the state with count: 0
  2. Then, we define the decrement and increment actions that return the new state with the respective changes — in this case only the count property of the state
  3. Finally, notice how the view code is exactly the same as the stateful implementation using Mithril, except the state and actions are extracted from the function parameters using ES6 destructuring instead of being defined in the closure

Hyperapp turns out to be great for small, lightweight applications. Maintainers are still changing some APIs and concepts as the framework is gearing towards a stable 1.0 release.

The main obstacle I ran into was composing multiple components, each one having its own state and actions. If you go through the hyper-long discussion in this GitHub issue, achieving that seems challenging — some people have done it, while others have tried and failed miserably.

It would turn out I was of the latter group.

Conceding defeat

Hyperapp easily allows extracting Counter as a stateless component:

However, because state and actions have to be defined in the app({ ... }) call, child component can’t have state or actions of their own — only lifecycle events. This means we have to manually pass values and functions as parameters to our child components — for an example of that, look no further than the Hyperapp TodoMVC implementation.

It also doesn’t allow us to easily put one Hyperapp-lication (what else would you call it?) inside another, meaning the architecture isn’t fractal, as André Staltz would say.

After getting stuck on component composition and some additional issues with both the Hyperapp router and TypeScript, I decided to keep using Mithril for now.

UPDATE: The team has done a great job at addressing some of the concerns I had by introducing state slices in v0.14.0. I was, in fact, so impressed with their solution, that I am now rewriting my portfolio website with Hyperapp!

A new hope

That didn’t mean I couldn’t try implementing Hyperapp’s architecture in Mithril! I kind of liked returning the state changes instead of reassigning state variables, and defining state, actions, and lifecycle events separately.

Let’s start with our Counter component from before:

The goal is to keep the view exactly the same while at the same time avoiding state mutations. To do this, we can write an abstract function that creates the component for us using the closure syntax:

Basically, what this does is:

  1. Defines a variable that contains the local state, merging the initial value we specified with the vnode attributes
  2. Creates local proxy actions, giving every function access to the current state and other proxy actions, and merges the value of the state with the result of the function, if any
  3. Returns an object which includes the Mithril lifecycle events (defined in events) and a view function that is called with the vnode, current state, and proxy actions

While inside the component function we technically do perform a mutation by reassigning state, this is hidden from the end developer using it. We can now write the definition for our Counter component without manipulating the state directly:

Hey, that’s actually exactly the same as the Hyperapp code! And we can still compose multiple components (or whole applications) together just like in regular Mithril, while at the same time writing separate definitions for each component!

Conclusion

Still Mithril

Of course, this is a naive implementation, lacking many features. If you’re already familiar with Hyperapp, you might have noticed certain differences — for example, lifecycle events, or in the view function —vnode being the first parameter, with state and actions taking second and third place, respectively. After all, this is still Mithril and we need to follow its basic rules.

Can’t access state in lifecycle events

Thanks to Maxi Dello Russo’s comment, I realized that lifecycle events like onupdate don’t actually have access to the up-to-date state of the component. To remedy this, we can merge the new state into the component’s state by using Object.assign(vnode.state, state) in the setState function. Alternatively, we can create proxy functions for lifecycle events, similarly to how we handle actions.

Not serializable

The application’s state is not stored in a single object, meaning we can’t easily implement undo / redo or time travel debugging.

Not completely type-safe

Being a heavy TypeScript user myself, I was unsuccessful in creating a completely type-safe version of this function (see the implementation I have so far). Because action proxies have different parameters than the base actions, we need the return type of a function as described in this proposal. Unfortunately, this feature is not officially available yet, which means action parameter types can’t be enforced reliably.

Not battle-tested

Overall, I like writing components with this architecture, but I will have to use it in more real world scenarios to better gauge its utility. For now, I’m just happy that Mithril is so flexible and doesn’t force me to only do things one way!

Feedback

Have I missed something obvious? Reinvented the wheel? Are you doing something similar — with or without Mithril?

Let me know in the comments!

Resources

Part 2 of this series explores using Redux for storing and updating local component state, and compares the resulting architectures.

--

--

Vlad Sabev
Vlad Sabev

Written by Vlad Sabev

🔧 DIY builder & web tinkerer

Responses (1)