Exploring Unidirectional Components in Mithril (part 1 — Hyperapp)
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:
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:
- We first initialize the state with
count: 0
- Then, we define the
decrement
andincrement
actions that return the new state with the respective changes — in this case only thecount
property of the state - 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:
- Defines a variable that contains the local state, merging the initial value we specified with the
vnode
attributes - 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 - Returns an object which includes the Mithril lifecycle events (defined in
events
) and aview
function that is called with thevnode
, 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
- An updated (and evolving) implementation of the
component
function with support for asynchronous actions - A more complex component — Stopwatch
- All examples used across this series of posts
Part 2 of this series explores using Redux for storing and updating local component state, and compares the resulting architectures.