Solid.js, the best of Vue and React
I recently created a new website and came across SolidJS, a relatively new web-framework. What amazed me was the fact that it combines the good things from React and Vue, leaving away the bad parts. For future projects, if it's my decision alone, I will consider it as a replacement for React.
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 contributors? 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 |
|
|
Bad |
|
|
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
<!--suppress ALL -->
<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 subcomponent, I have to write a new file, rather than keeping everything in one file first and moving to a separate file later.
- You cannot just compile the file with the TypeScript compiler. You have to use a special Vue compiler instead and set up 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
Let’s 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.
<!--suppress ALL -->
<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
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 run every time the component re-renders. Internally, React makes and effort to ensure that the correct values for
useState
anduseRef
. It stores all values in a global array and recreates them when the component renders. This feels way to complicated and the Vue mechanisms 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 useuseCallback
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 and 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 looked 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 playground:
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 depends on the value. That way, it can re-render components,
run effects and so on. There is a great
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)
- vue.js@3.2.45: 33.9kb
- react-dom@18.2.0: 42kb
- solid-js@1.2.1: 6.4kb
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.