Solid.js, the best of Vue and React

January 04, 2023

Last year a colleague told me about the JS Party podcast and the first episode I listened in was about Astro.build. I got pretty amazed by that framework, but that is another story.

Then, recently, I wanted to try something with Handlebars again, have a look at https://handlebars-ng.knappi.org/ if you like, but that is also another story. I decided to use Astro to build this website and when I looked for a framework to implement the interactive parts, I stumbled across https://solidjs.com. And what I saw there blew my mind.

In the past three years, I worked a lot with React and Vue.js and I in my mind I built a list of goods and bads for both of them. And now suddenly, I stumble across a framework that I have never heard of before and that removes all the bads from both? And it is already five years old, has a core team of 7 and lots of contributers? Why now…

The goods and bads of React and Vue

Let me back up at little and tell you what I consider the goods and bads of React and Vue, technologically.

React Vue.js
Good
  • TypeScript support
  • Flexibility of JSX/TSX
  • The way composables work
Bad
  • The way hooks are implemented
  • TypeScript support

JSX/TSX versus Vue

In React, you write your components in JSX/TSX which is a superset of JavaScript/TypeScript. For me it seemed awkward at first, writing XML inside JavaScript, but that is something I quickly got used to. The good thing about this is that it allows you to use all the flexibility of a real programming language to write clean code. You can extract functions, variables and use constants.

In addition, JSX has got a lot of support from TypeScript developers. This is a small component from my project Gachou.

import React, { ChangeEvent, ReactNode, useRef } from "react"
import { generateId } from "src/utils/generateId"

export interface MyFileButtonProps {
  children?: ReactNode
  multiple?: boolean
  onChange?: (files: FileList) => void
}

export const MyFileButton: React.FC<MyFileButtonProps> = ({
  children,
  multiple = false,
  onChange,
}) => {
  const htmlId = useRef("filebutton-" + generateId())

  function handleFileSelect(event: ChangeEvent<HTMLInputElement>) {
    if (onChange) {
      onChange(event.target.files ?? new FileList())
    }
  }

  return (
    <label htmlFor={htmlId.current} className={"btn btn-primary"}>
      {children}
      <input
        multiple={multiple}
        id={htmlId.current}
        style={{ display: "none" }}
        type={"file"}
        onInput={handleFileSelect}
      />
    </label>
  )
}

The beauty of this is that you just write TypeScript with some syntactic sugar for building DOM. When you use this component, TypeScript will take care of auto-completion and validation.

The common way to write this same code in Vue.js would look like this

<script setup lang="ts">
import { generateId } from "./generateId.ts"
const props = defineProps<{
  multiple: boolean
}>()

const emit = defineEmits<{
  (e: "change", files: FileList): void
}>()

function handleInput(event) {
  emit("change", event.target.files)
}

const id = "filebutton-" + generateId()
</script>
<template>
  <label :for="id" class="btn btn-primary">
    <slot />
    <input
      :id="id"
      :multiple="props.multiple"
      style="display: none"
      type="file"
      @change="handleEvent"
    />
  </label>
</template>

I have to admit that I have not used Vue 3.x and script setup a lot yet. I don’t know how well TypeScript is supported right now. In Vue 2 there was much room for improvement. But there are some pain points that I have with the Vue component structure:

  • In order to extract a sub-component, I have to write a new file, rather than keeping everything in one file first and moving to a seperate file later.
  • You cannot just compile the file with the TypeScript compiler. You have to use a special Vue compiler instead and setup your IDE with Vue extensions. This introduces more complexity and possible points of failure.
  • The script setup method is convenient, clean and I like it very much compared to the old Vue component style. But it also introduces magic and I never know exactly what the compiled result actually is. With TSX it is much easier to track method calls, variable access, type usage, as long as the IDE supports TypeScript.

I am aware that you can use JSX/TSX for Vue components as well, but this is not common and I don’t know how well supported it is.

For me this is one point in favour of React, but let’s look at the other one

React Hooks vs Composition API

Lets have a look at the trivial “counter” example. In order to show my point, I will add a console.log-statement into the function

import { useState } from "react"

export const Counter: React.FC = () => {
  const [current, setCurrent] = useState(0)

  console.log("current value", current)

  function increment(): void {
    setCurrent(current + 1)
  }

  return <button onClick={increment}>{current}</button>
}

and the same thing in Vue.

<script setup lang="ts">
import { ref } from "vue"
const current = ref(0)

console.log("current value", current.value)

function increment() {
  current.value++
}
</script>
<template>
  <button @click="increment">
    {{ current }}
  </button>
</template>

You can have a look at the Vue example in the Vue SFC playground and the React example on codesandbox.io

When you run the examples, you will notice that React logs to console each time you click on the counter. Vue on the other hand just logs once, when the component is created. And that’s what I don’t like about React hooks:

  • The complete component function is ran every time the component re-renders. Internally, React makes and effort to ensure that the correct values for useState and useRef. It stores all values in a global array and recreates them when the component renders. This feels way to complicated and the Vue mechanims are much simpler.

  • When using async code in the component, you may get old state values. When adding

    setTimeout(() => console.log("increment", current), 1000)

    to the end of the increment function. The old counter value is logged after one second, because that is the value in the current function context (see CounterWithTimeout.tsx

It also means that you have to specify dependencies for effects. In order to

  • You have to explicitly define dependencies for useEffect and if you want to extract functions, you have to use useCallback to make sure that the function is only recreated when its dependencies change, which you also have to specify explicitly.
  • I have situations where forgetting a useCallback lead to an endless loop (although I cannot reproduce that anymore)

This was actually better when everybody was using class-based components with an explicit render-function. But I don’t think anybody wants to add this boilerplate anymore.

The reason for this complicated approach is historical. React had function-components before, but didn’t allow state in there, so the whole component was just a render function.

So, how is Vue solving the reactivity problem. What is it doing better?

It wraps your primitive values with a ref in order to detect changes. This means, that you cannot just use counter to access the current value, but you have to use counter.value. What Vue hides from you is that the counter is actually a JavaScript Proxy an the access to .value is intercepted by Vue.

That way, Vue can determine the dependencies of effects automatically. It can find out, which parts of the component need re-rendering, when a value changes.

I haven’t look at the source code of Vue.js and React in detail, but I can imagine that the Vue approach can provide a much simpler implementation. It is certainly less error-prone for the user.

SolidJS: TSX/JSX with setup functions

SolidJS combines the best of both worlds:

The code looks very similar to React here is the counter-example from the SolidJS playgrond:

import { render } from "solid-js/web";
import { createSignal } from "solid-js";

function Counter() {
  const [count, setCount] = createSignal(1);
  const increment = () => setCount(count() + 1);

  return (
    <button type="button" onClick={increment}>
      {count()}
    </button>
  );
}

render(() => <Counter />, document.getElementById("app")!);

You can pretty much map the hook-functions to functions in Solid-JS

useState -> createSignal
useMemo -> createMemo
useEffect -> createEffect
useLayoutEffect -> createRenderEffect

Unlike React however, the function is a constructor function like the setup-function ins Vue.js. It is called once when the component is instantiated. This is possible, because createSignal does not return the value of the state directly, but a function to retrieve that value. When the value is retrieved, for example while rendering the component, SolidJS tracks which parts of the code dependend on the value. That way, it can re-render components, run effects and so on. There is a gread video that explains all this in detail.

I have not run any benchmarks myself, but I can imagine that this is simpler and faster than what React does. According to their own website, they are twice as fast as React…

The size of the framework speaks for itself (minified + gzipped)

Will I use SolidJS in the future?

For personal projects and websites, certainly.

At work, there are more things to consider:

Josh Collinsworth noted that “React isn’t great at anything except being popular”. and he certainly has a point, but using a popular framework also gives you access to a lot of open source libraries. Code that you don’t have to write yourself. This can be a good thing, it can also get out-of-hands. But it is something to consider. I have yet to find out, how large the SolidJS ecosystem is.

The more important thing is: In my daytime job, I usually work in a team. I cannot (and don’t want to) make such decisions on my own. In general we apply the rule of three:

Three people in the team must be able and willing to fix code that was using a given technology, otherwise we don’t use it.

But I will show SolidJS to my colleagues at least…

Conclusions

React and Vue.js are both great frameworks with vibrant communities. At work, we create large apps and sites with both of them and it really makes our live easier.

However, it is important to look out for new frameworks that appear all the time. React and Vue.js have introduced concepts and ideas that those frameworks use. At the same time, they can be leaner, because they don’t need to be compatible.

SolidJS combines the good parts of React (the more or less standardized JSX/TSX format) with the good parts of Vue.js (the simple state management). It is small, fast and looks “solid”. I would suggest any React or Vue developer to have a closer look. I have yet to find out how large the ecosystem is and give it some real-life testing. But for the moment, that will be restricted to private projects.


Profile picture

Written by Nils Knappmeier who has been a full-time web-developer since 2006, programming since he was 10 years old.