
Does a frontend framework matter?
As much as you might have heard otherwise, the answer is an astounding yes. Those who fight on the hills pretending it’s all about the code are missing one very crucial point:
Frameworks shape the code you write.
To a certain extent, they define what’s possible, but more important than that – they set a path of least resistance. Yes, frameworks coming and going is one of the most frequent occurrences in nature, but that doesn’t mean it’s wasted effort to choose the right one or invest in it. The waste only appears when the frameworks are of the same kind, and introduce similar tradeoffs.
The clearest example of this is Elm. It’s not a coincidence that Elm manages state differently than how you’d normally do it in JS-land. Even Redux, which is an implementation of The Elm Architecture (also known as Model-View-Update), could not give the same streamlined experiences developers expect from Elm. Even if that experience is better than what’s otherwise available in the JavaScript ecosystem, you have to be fairly disciplined to keep it going.
Alas, there is only so much time in this world for us to try and fit a square into a peg. At this point some would fall back to the usual default: “Elm is great, but it’s too far from JavaScript. And there’s really nothing very different about all the JavaScript options, so I’ll just – ”
But wait. What if I told you there is another option? As robust as Elm, with a familiar syntax, and that you could learn in a day?
Grab yourself some tea, for this is the story of how adopting the Gleam language out of pure disdain for TypeScript led to much better state management for Nestful.
Before we delve into the details, let me bring you up to speed.
About a year since I started experimenting with Gleam, a delightful functional language that compiles to both Javascript and Erlang. I’ve decided to rewrite Nestful in Gleam to alleviate some of the terrible slog that is TypeScript.
The first major part was the introduction of Vleam, a set of tools that allows one to write Gleam inside Vue SFCs. Nestful is written with Vue and incremental adoption is a must when working on such a large-scale rewrite.
In November I launched OmniMessage, an experimental library for client-server communication. OmniMessage, you see, is an extension of Lustre (please bear with the buzzwords), Gleam’s Elm-inspired frontend framework. It allows one to define messages that are common to both the client and the server, thus streamlining communication.
Whether that’s a good idea or not, time will tell. What I do know is that Lustre was a breath of fresh air. But why? What is it about Lustre that’s missing from Redux (except for my strong, deep, unwavering dislike of React, that is)?
The answer is Gleam. Writing MVU-style apps in JavaScript is a bit like driving in a zigzag pattern — possible, but unless you’re 100% diligent, a crash is coming.
Luckily, I try to avoid premature optimization as much as possible and state management was no different. Nestful’s was simply done, by hooking up different kinds of Vue composables and utilizing Yjs’ event system. Some people may gasp at the lack of structure, but hindsight has revealed this to be a good decision. Much better than committing to what some would consider “proper” state management, spreading its tentacles into every corner of Nestful, making change a labor of desperation.
Instead, having a simple, unstructured system allowed it to be promptly removed when the learnings were sufficient and time had come to replace it.
After adopting Gleam for the language it is, Nestful could continue to enjoy its ecosystem, including Elm-inspired Lustre.
Why you should have some TEA
The Elm Architecture (TEA), also known as Model-View-Update (MVU), is a centralized form of state management made popular by —you guessed it —The Elm programming language.
As its alternative name suggests, it involves a centralized model that is sent for rendering to a view function, and is updated by an update function. If it sounds simple, that’s because it is. Here’s the state management diagram straight out of Elm’s docs:
Here’s the counter example from Lustre, Gleam’s frontend framework, which also implements TEA:
type Msg {
Incr
Decr
}
fn update(model, msg) {
case msg {
Incr -> model + 1
Decr -> model - 1
}
}
fn view(model) {
let count = int.to_string(model)
div([], [
button([on_click(Incr)], [text(" + ")]),
p([], [text(count)]),
button([on_click(Decr)], [text(" - ")])
])
}
That’s it, kids. For most web applications, you can pack Pinia and Zustand up, thank them for their service, and promptly show them out to the nearest commit.
You see, those popular solutions are popular for good reason. They are a low common denominator that, with all honesty, works quite well. The problem is they’re not as good as it gets. They break at the edges, if you will. A local maxima.
For Nestful, The Elm Architecture has the edge on them in three crucial places.
Preventing state-creep
State creep is the bane of my existence. When components are stateful, one of two things must happen:
You must hold constant, unwavering diligence, keeping local state local and global state global.
You despair as global and local states are inevitably mixed into a soup of misery.
I personally am not a fan of either constantly looking over my shoulder for the state boogeyman, nor of holding the mental model the size of the Tokyo rail system of how state flows through an application.
Instead, using TEA, I have a lovely, detailed map of all the paths data flows in. That map is an entire overview of the application, caging in the global state, only to be seen — never mutated.
A pure heart
Another great advantage of strictly enforcing TEA is that it makes it very easy to structure an application using “Functional Core, Imperative Shell”, a design pattern in which I/O is separated out to distinct parts at the start and end of a flow, while the center, the business logic processing that I/O, is kept pure. It always returns the same output for a given input, and does not involve side effects.
As it so happens, that is exactly how TEA pushes you to architect your state. Both the update and view functions are pure, and will return the same output for a given model and message.
Side effects are then handled by an effect system, which takes side-effect tasks returned by the update function and executes them out of the update function’s context.
This approach isolates the important bit of the application, the business logic, from the ever-changing, hardly testable, outside world.
Works great with Gleam
Gleam lends itself to this model quite significantly. Being a functional, type-safe language is a great usability gain for TEA, especially thanks to discriminate unions, who allow us to write the apps message type like this:
type Msg {
Incr
Decr
}
And the case statement, that allows for exhaustive matching on that message type:
fn update(model, msg) {
case msg {
Incr -> model + 1
Decr -> model - 1
}
}
Other implementations, like Redux, Dartea, or Vuex to some extent, while admirable, all fall short. After a developer has chosen a paradigm, tools need to not only enforce it, but encourage that paradigm. They need to not only make it the path of least resistance, but make the other paths hard in comparison.
A tool that requires diligence to keep going is failing at its job.
An added bonus from those three advantages is they make it extremely easy to refactor. They allow razing fields of old code to the ground, building skyscrapers above them, exchanging the fear of imminent crumble for the guarantees of type-safety and robust defaults.
But wait, isn’t Nestful a Vue app? How am I going to fit Gleam-written TEA into it? Am I going to have to settle for a TEA-like experience, similar to what I could conjure up using existing TypeScript solutions?
Incrementally adopting a frontend framework
So I’ve adopted Gleam and can now write it inside Vue files instead of TypeScript and look to use it to implement TEA and replace the current haphazardly-managed state.
To do that, two main tasks need to be completed:
Construct a runtime capable of executing the TEA state machine.
Somehow integrate that runtime into existing Gleam and TypeScript Vue components.
The first one already exists, it’s just Lustre (but without a view).
There are two ways of going about incorporating Lustre incrementally. The first is to carve out portions of Nestful and write those exclusively in Lustre. The second is to launch a Lustre app and render nothing, using just its state management capabilities.
Since the goal here is to have comprehensive state management, the second solution is more appealing, but a closer look will reveal that it’s actually the only solution possible.
Nestful, you see, is a giant item graph that’s traversed for specific views or actions. To enable offline-only capabilities, the entirety of the graph is stored on the client in the form of a single Yjs document, and if enabled, is synced on changes.
This means that not only is there very little local state to manage, the global nature of Nestful’s state will greatly benefit from collecting the static logic into a single, coherent, state machine. For Nestful, it makes much more sense to embed Vue in Lustre rather than Lustre in Vue.
// /src/compositions/useNestful.ts
import { computed, shallowRef, watch } from 'vue';
import { UseNestful } from '../nestful/compositions.gleam';
import { nestful } from '../nestful.gleam';
const modelRef = shallowRef();
const modelComputed = computed(() => modelRef.value);
const [initialModel, dispatch] = nestful((model) => {
modelRef.value = model;
});
modelRef.value = initialModel;
export function useNestful() {
return new UseNestful(modelComputed, dispatch);
}
// /src/nestful/compositions.gleampub type UseNestful {
UseNestful(model: vue.Computed(Model), dispatch: fn(Msg) -> Nil)
}
@external(javascript, "/src/compositions/useNestful", "useNestful")
pub fn use_nestful() -> UseNestful
This is the glue code that allows Vue components, written either in Gleam or TypeScript, to read the model and dispatch messages. It is supported by helper functions in nestful.gleam to avoid having to import the different Gleam types into TypeScript.
Vue components are then treated as functions composing a Lustre app’s view function.
But there’s a catch.
The Elm Architecture’s centrally managed state trades off component composability to the point where Elm’s docs outright discourage them. While I don’t consider this blasphemy like some React folks may, I can see how different parts of an app lose on not being completely separate.
All things considered, it’s not an endemic issue similar to those I am trying to solve with the move to Gleam. That same page on the Elm docs discouraging components lists much more important things right before that that are benefits of TEA.
Which brings us to today.
A fun future
Except for two pending files, no global state is managed outside Lustre. State holding composables have been reduced significantly, and I hope to remove them completely in the following months.
The adoption of TEA has made Nestful faster, more maintainable and easier to develop features for. Most importantly, however, those advantages are now the path of least resistance in Nestful’s development cycle.
I hope that this transition will continue to bear fruit as time passes. By reducing the need for testing, by encouraging better design and architecture, and most importantly — by being fun.
This post was mostly about the technical benefits of Gleam and Lustre, but there’s also an emotional one — programming in them is a much, much, much more enjoyable experience than the TS alternative.
As we put our best foot forward in an attempt to increase value for our customers we sometimes forget about ourselves, and if we can — we can and should enjoy the process. Programming can and should be fun.
Subscribe and stay tuned for more unique takes on personal productivity and novel(-ish) web technologies.