JS Component State of the Art — Part 1

I’m currently big on avoiding tribalism with regard to frameworks, and wanted to take a look at the component syntax in four major frameworks.

Matt Burgess

--

Every framework has its own syntax and approach to components. These approaches change over time and the last year has been one of fairly substantial shift across the industry, with major players replacing large sections of API. It can be difficult to keep a finger on the pulse of each of these technologies, and know what the current state of the art is.

I thought I’d sit with each and take a look, see how they compare in their implementation of the most trivial of components in the first instance.

The Contenders

The examples written here will be done in four frameworks: React, Vue, Ember and Svelte.

All of these frameworks’ component invocations are are in some way new, albeit to greater or lesser extents. Implemented here are React’s newish Hooks API, Vue3’s upcoming Component Composition API, Ember’s Octane syntax, and Svelte is… well… just new.

If you’re wondering why I skipped Angular, which has just hit Version 10… Honestly I’m sorry but I’m just not that interested in Angular.

The Simplest Possible Reactive Component

This is the typical demo case, a counter with a few buttons that can increment and decrement it.

I’m actually not a fan of this example as it is so simplistic that it can provide a misleading gauge of the syntax by comparison to a more fully-featured component. If it makes easy things easy, but moderate things turn into a tangled mess, that’s not a good choice. In particular it doesn’t look at how the framework works with props.

Still, let’s take a look at this simple case, and get a baseline for implementation of a component in each.

React

The addition of the Hooks API made it possible to have basic state handling in a React functional component, making simple but interactive components a lot terser and neater.

import React, { useState, Fragment } from 'react';  export default () => {
const [counter, setCounter] = useState(0);
const increment = () => setCounter(counter++);
const decrement = () => setCounter(counter--);
return <Fragment>
<span>{counter}</span>
<button onClick={increment}>increment</button>
<button onClick={decrement}>decrement</button>
</Fragment>
}

Some liberties were taken here, as it would be just as likely to put the increment and decrement code inline in the onClick functions. Still, I wanted parity between these implementations where possible.

In “lines of code” terms, React gets a slight advantage because of JSX just being dumped straight out instead of having a separate template section.

Vue

The below code isn’t current Vue, but it will be soon enough. This is the Vue 3 Composition API.

<template>
<span>{state.count}</span>
<button @click="increment">increment</button>
<button @click="decrement">decrement</button>
</template>
<script>
import { reactive } from 'vue';

export default {
setup() {
const state = reactive({count: 0});

function increment() { state.count++ }
function decrement() { state.count-- }
return { state, increment, decrement };
}
}
</script>

I think of all of these, the Vue example is the one that is the least clear to me personally. The use of a setup function seems like a strange bit of indirection and I feel like the need to actually return all of the current state and scope provides a future maintenance burden. Note that everything else just sort of sets up the context and just works.

Interestingly Vue is also the only framework in this example that needs to consider its count as explicitly state. Everything else just dumps values in the current scope. This is much like React class components used to do with this.state.counter but hooks do away with that.

Edit: Since I wrote this it looks like there has been an update to the Vue approach. If I’m interpreting the new approach correctly, the syntax above could shrink down to the following

<template>
<span>{{ state.count }}</span>
<button @click="decrement">decrement</button>
<button @click="increment">increment</button>
</template>

<script setup>
const state = reactive({count: 0});

export const increment = () => state.count++;
export const decrement = () => state.count--;
</script>

This is my interpolation of two different syntaxes, one of which used Vue’s ref function which I’m not a fan of, and one using reactive. Hopefully this is valid. If not, the only real difference is that instead of const state it directly sets const count = ref(0), then it updates count.value.

It actually looks remarkably similar in some ways to the Svelte example at the bottom there.

Ember

An interesting comparison in large part because Ember is a fully featured, batteries-included framework, by comparison to these more focused libraries that are solely component driven. (Ember has a bunch of other abstractions such as Routes that handle specific things.)

The Ember example is the only one where multiple files are used. Ember splits the template from the backing code, which has advantages and disadvantages that are out of scope of this discussion.

This means we have the following JavaScript code:

import Component from '@glimmer/component';
import { tracked } from '@glimmer/tracking';
import { action } from '@ember/object';

export default class CounterComponent extends Component {
@tracked count = 0;

@action increment() { this.count++ }
@action decrement() { this.count-- }
}

And then the template:

<span>{{this.count}}</span>

<button {{on "click" this.increment}}>increment</button>
<button {{on "click" this.decrement}}>decrement</button>

In terms of LoC, current Ember is surprisingly short. Some considerations should be given to the fact that a lot of Ember boilerplate is typically auto-generated, but in this case that would only account for about three lines.

Lines of Code is not a great metric on its own. Twelve lines of clear and expressive code is better than five lines of gibberish. But all things. being equal, terser code should be preferred.

The use of annotations is a nice feature, and just making all class properties the current state is cool. That said, Ember is also the only framework using a class, which feels a little old-school.

Where Ember starts being less familiar is the slightly odd action invocation, which is a real departure from the others. As someone doing Ember for years I’m au fait with helpers, but they might be jarring to others less familiar.

The bigger issue is the namespacing. Note above that the tracked decorator comes from glimmer/tracking while the action decorator comes from ember/object. The distinction between Glimmer (the rendering engine) and Ember (the framework) is not something that should be leaking in here, in my opinion.

Svelte

Definitely the newest kid on the block, it’s worth looking at Svelte for two reasons. First of all, it’s a very different approach to any other framework, and often radically so. But mostly because when you look at the code even if you’re unfamiliar with the codebase it’s often bizarrely small.

<script>
let count = 0;
function increment() { count += 1 }
function decrement() { count -= 1 }
</script>
<span>{count}</span>
<button on:click={increment}>increment</button>
<button on:click={decrement}>decrement</button>

The lack of boilerplate and arcane syntax needed for this simple case is impressive. Not only is there very little of it, but it’s almost childishly simplistic.

Contrast that to all of the above. Not a thing imported. No abstractions to be learned. No arcane invocations, no complex syntax.

The bigger question, then, is does it scale? Does it turn into syntax soup when you add multiple state items and some props? I should probably find out!

Conclusion

Any conclusion would be premature. There are things I like and don’t like about each of these approaches. Svelte’s oddly global scope is very weird and feels objectively wrong, but it actually makes for simple, highly comprehensible components. I actually like Ember’s templating approach, and always have preferred it to JSX or the more Angularjs-derived approach of Vue. React’s pure JavaScript implementation makes it the most optimisable and customisable, but full of traps for new players, and IMO the hooks API isn’t a particularly intuitive one.

This isn’t the end, though. I just didn’t want to get bogged down in a week of research before I post, but really what matters most is a more complex component. A second test with an interactive component that is passed properties, and handles forms.

--

--

Matt Burgess

Senior Web Developer based in Bangkok, Thailand. Javascript, Web and Blockchain Developer.