Comparing Front End Frameworks cover image

Comparing Front End Frameworks

...the silly way.

I have been immersed in the JavaScript world for nigh on a year now, and have recently been exploring frontend frameworks in hopes of finding something I enjoy developing in. On my journey I have dabbled in various frameworks and enjoyed the learning. When comparing the developer experience between the different frameworks; one, silly, idea that crossed my mind was throwing some of them together in a single webapp. Otherwise called a monolith, or a collection of microfrontends.

What is this?

In essence, these are my thoughts on creating a small counter in four frameworks: React, Svelte, Vue, and Imba (technically a compiled language).

The page is hosted here: counterfyi.netlify.app

If you just want to see the code, you can do so here.

From 💡 to web

The idea to build a mini counter app came as I found counter.fyi while trawling domain registrars for a new collectible (it's only a hobby, I swear). The idea was simple enough, but as always, execution... to say that it was overkill is an understatement. I ended up using Podium (a microfrontends library developed by finn.no; an online marketplace local to my country—which is always exciting), and then dockerize the individual components in a monorepo.

Yikes!

The process deserves its own story. Let's instead dive into some code.

A Counter App in Four Frameworks

A counter is, by and large, one of the most common ways to get a feel for a framework. So much so, that Vite includes it in their scaffolded projects.

To make it a bit more interesting I decided to add shared state management between the components. For this I relied on Podium's MessageBus, building a small RxJS observable that each component implemented (I will write about it in a future post detailing the journey creating this experiment).

Another thing to note is that css was only implemented as a shared css file (since every component shares). So no comparison will be done on css, though it is certainly warranted when it comes to DX (Maybe I'll come back to do it and update the post at some point).

Quick disclaimer: I did set out to write each component as idiomatically as possible. I may have failed in that endeavour in some instances, don't shout at me.

React

Being an industry heavy (and the uncontested leader), it is no surprise that I decided to include it. I have dabbled some in React, but never felt I connected with it. Writing the component was, however, undeniably easy. And the only snag I hit was implementing the observable (beware the infamous useEffect infinite loop).

import React, { useEffect, useState } from "react";
import { globalCounterSub } from "../__services__/message.service";

function App() {
    const [count, setCount] = useState(0);
    const globalCounter = globalCounterSub("react-pod");

    useEffect(() => {
        const subscription = globalCounter.subscribe((n) => setCount(n));
        return subscription.unsubscribe;
    }, []);

    const decrement = () => globalCounter.update((n) => --n);
    const increment = () => globalCounter.update((n) => ++n);

    return (
        <article className="React">
            <button className="decrement" type="button" onClick={decrement}>
                &ndash;
            </button>
            <div className="number">{count}</div>
            <button className="increment" type="button" onClick={increment}>
                +
            </button>
        </article>
    );
}

export default App;

Another thing to note is that I wanted to make each component as similar as possible in terms of how and where I declared and called functions and implemented the shared state.

Most of DX comes down to the boilerplate and syntax you write initially. Then as the app grows or you work in a team on a larger codebase, readbility and maintanability begin to emerge as the single most important factors. React favours immutability to better facilitate maintenance and predictability (all of these factors influence each other). To my mind components in React are best when they are lean and small. See a component grow much larger than the component I wrote above and your eyes will start to glaze over as you attempt to comprehend which function affects what reactive state where in my opinion this is largely because React doesn't separate areas of concern well.

Svelte

As I have explored these frameworks, this one has stood out as one I would like to use more. Svelte is different from React, and Vue (see below), in that it requires a compilation step (honestly that does not describe it well as we usually bundle React and Vue builds and so the difference is rather imperceptible). What this allows Svelte is to work with the real DOM as opposed to a virtualized one (and as such gain a considerable increase in speed when updating the DOM).

<script>
    import { globalCounterSub } from "../__services__/message.service";

    const count = globalCounterSub("svelte-pod");
    const decrement = () => count.update((n) => --n);
    const increment = () => count.update((n) => ++n);
</script>

<article class="Svelte">
    <button class="decrement" type="button" on:click="{decrement}">&ndash;</button>
    <div class="number">{$count}</div>
    <button class="increment" type="button" on:click="{increment}">+</button>
</article>

That's probably as terse as it gets following the style guide I set for myself.

You are probably noticing how I do not have to do anything except register the namespace of the component with the observable. This is because Svelte supports RxJS observables out of the box. In the React component I had to use the useState hook to store the variable, and call setState in the subscription callback. With Svelte this is all handled for us. Another neat thing is the separation of concerns. There is a clear delineation between what is imperative logic, and what is declarative state.

The $ in {$count} is used to indicate to Svelte that this is a store value, and not the subscription itself. It is akin to a callback like this n => $count = n being passed to the subscription, assuming let $count; has been declared.

Vue

Vue has been my go to for some time. It is very similar to Svelte in terms of its separation of concerns (and does it a tiny bit better in my opinion). However, like React, it utilizes a virtual DOM. The issues I have with it at the moment is much the same I have with React, there is an old way to do things and a new way. In Vue this dichotomy is more pronounced in the old Options API and the new Composition API.

<script setup>
    import { ref, onMounted, onBeforeUnmount } from "vue";
    import { globalCounterSub } from "../__services__/message.service";

    defineProps({
        count: Number,
    });

    const count = ref(0);
    const globalCounter = globalCounterSub("svelte-pod");
    let subscription;

    onMounted(() => {
        subscription = globalCounter.subscribe((n) => (count.value = n));
    });
    onBeforeUnmount(() => subscription.unsubscribe);

    const decrement = () => globalCounter.update((n) => --n);
    const increment = () => globalCounter.update((n) => ++n);
</script>

<template>
    <article class="Vue">
        <button class="decrement" type="button" @click="decrement">&ndash;</button>
        <div class="number">{{ count }}</div>
        <button class="increment" type="button" @click="increment">+</button>
    </article>
</template>

Despite having a very clear separation of concerns. The <script> section is muddied by lifecycle hooks and having to define props to make sure this are passed into the template. Defining props this way is only necessary when using the setup modifier on <script>. The alternative would be to declare an object containing all the props and lifecycle properties as a default export (a considerably more messy-looking implementation). Had it not been for the DX that <script setup> provides I would likely not be as invested in Vue.

Imba

And now for something completely different. Yes, Imba is not a framework, and yes it is also the brainchild of a fellow countryman. Imba 2.0 is currently under development and it is looking a lot more mature than its first incarnation. Imba uses a memoized DOM as opposed to a virtual one. There are considerable efficiency improvements to DOM rendering speeds if we are to believe one of its creators. What I like about Imba is that it takes a lot of inspiration from other langauges, but also attempts to reconcile imperative logic with declarative state creating almost no separation of concerns. And you would be excused for thinking "but that just cannot work!". Yet somehow it does.

import { globalCounterSub } from "../__services__/message.service.js"

let count = 0

let globalCounter = globalCounterSub('imba-pod')

globalCounter.subscribe do(n)
    count = n
    imba.commit!

def decrement do globalCounter.update do(n) --n
def increment do globalCounter.update do(n) ++n

def app
    <article.Imba>
        <button.decrement @click=decrement> "–"
        <div.number> count
        <button.increment @click=increment> "+"

imba.mount app, document.querySelector("#imba-pod")

Seeing that you probably have a lot of questions. I would advise taking a brief look at the documentation as you try to make heads or tails of this. Or maybe you don't need to. If you are familiar with Ruby or Python this shouldn't look all too foreign. That said, diving into Imba will invariably mean learning a lot of new syntax and grammar. You also treat the html equivalent as just another part of the language. Which, unlike JSX, means that you can be a lot more creative with how you create your components (once you get used to all the quirks this language offers).

All in all I like Imba and am excited to see where it will go in the future. Elm is another contender in this arena that I have yet to take a look at. It certainly seems a bit more imposing than Imba. Either way, as WebAssembly settles into its role on the web, and Custom Elements become more usable, these types of languages that blur the line between HTML, CSS and Javascript, will only become more relevant.

Conclusion

Going forward, I found that Svelte is the framework I am sticking to for the foreseeable future. That said, I am excitedly following the development of Imba, and I am by no means averse to working in React or Vue (they are great, but the foundations they are built on are not aging well). That said, JavaScript frameworks are a dime a dozen, a new one sprouting from the back of another every time a developer sneezes. I could have included Solid, Alpine, Lit, or Stimulus, to name a few. I have looked at all of them, and Alpine intrigued me, while Solid seems like something you will migrate your React project to at some point in the future.

"But wait! You haven't mentioned Angular." - I hear you scream in internal monologue. You are right, I haven't, and this is the last time I do.