SolidJS - a first look

I take SolidJS for a spin and compare it to Svelte in terms of DevX

· 8 min

I've been watching and reading about SolidJS for the past year. The articles by its author are full of knowledge about the inner workings of JavaScript frameworks. No doubt that Ryan, the author, knows his JavaScript and web. I believe he has been a member of the Marko team and that framework is the fast!

SolidJS prides itself by claiming that it's currently the fastest framework out there. Speed is often not the problem for most apps. Unless we are talking about low-powered devices. Network speed is also an issue. Large app and slow Internet connection is a bad combination. That's why we see all the SSR frameworks such as SvelteKit, Next, Nuxt and others gain in popularity. Hydration is cool but it also makes the code more complex and requires a server, well, or a cloud function in most cases.

Svelte prides itself by creating the smallest JS bundle. That doesn't apply if your app has many components, but is often not a problem unless your app is exceptionally large. Some time ago Ryan managed to optimize Solid's bundle size to be on par if not even smaller than Svelte's.

Svelte also prides itself by being used for low-powered devices. This got me thinking. If Solid is the fastest framework with the smallest bundle size it must be a perfect framework for low end devices and also in general. You get faster asset downloads and theoretically you should get more juice out of it because it's the fastest.

Another bonus, in my eyes, is that Solid does all this without virtual DOM.

But what about developer experience? How is it compared to Svelte? I decided to find out by porting my simple Svelte app to Solid.

The Sample Applicationpermalink

The sample application is simple, but has a few moving parts. Its primary purpose is to fetch and display a random quote from Kanye Rest API. It also has a timer to test HMR reloads, Tailwind CSS to test how it is to work with CSS in Solid and a fade CSS transition to test how this works in Solid.

SolidJS sample app screenshot

Here is a codesandbox to see it in action.

Boilerplace Setuppermalink

SolidJS has a nice set of Vite-based app starter templates. The Tailwind is supported by WindiCSS. I am actually not sure if it's Tailwind CSS anymore. By judging Windi's website it looks like is taken its own path to become a Tailwind alternative.

Anyway, for my project I chose the WindiCSS + TypeScript template as I wanted to see how autocompletion performs in the editor.

$ pnpx degit solidjs/templates/ts-windicss solidjs-playground

The main file is simple. I was first looking for the windi.css file until I found out that this file is virtual.

// index.tsx

import "windi.css";
import { render } from "solid-js/web";

import App from "./App";

render(() => <App />, document.getElementById("root"));

If you know a bit of React this code should look familiar.

Solid.js Componentspermalink

In Solid.js components are constructors. They are called once and then they cease to exist. This is how I understand it at least.

// App.tsx

import type { Component } from "solid-js";

const App: Component = () => {
return (
<p class="text-4xl text-green-700 text-center py-20">Hello tailwind!</p>
);
};

export default App;

Time.tsxpermalink

I wanted to see how good Solid's HMR is and for that I created a small timer component. All it does is to calculate the time difference in seconds from the app loads and now. For that I used the excellent date⁠-⁠fns library.

import { createSignal, createMemo, onCleanup } from 'solid-js'
import { differenceInSeconds, format } from 'date-fns'

export enum Interval {
OneSec = 1,
FiveSec = 5,
TenSec = 10,
}

interface TimeProps {
frequency: Interval
}

export const Time = (props: TimeProps = { frequency: Interval.OneSec }) => {
const loadTime = new Date()
const [spent, setSpent] = createSignal(0)
const timer = setInterval(
() => setSpent(differenceInSeconds(new Date(), loadTime)),
props.frequency * 1000
)
const timeSpent = createMemo(() => format(new Date(spent() * 1000), 'mm:ss'))

onCleanup(() => clearInterval(timer))

return (
<p class='px-2 py-1 text-center text-indigo-900 bg-indigo-400'>
You've just wasted <strong>{timeSpent()}</strong> of your life on Kanye
</p>

)
}

Solid signals are the basic reactive primitives. You can some what compare them to React's useState directives or normal Svelte variables.

Because Solid components are constructors and only run once you have to set up your component lifecycles. In the time component I used onCleanup method to cancel the interval timer.

To calculate and format the time in seconds I used createMemo directive. You can compare it to Svelte's reactive variables. I actually could have done it in a simpler way by only creating a function and not wrapping it in createMemo directive. createMemo is used for better caching for example to reduce work required to access expensive operations like DOM node creation. According to Solid's guidelines it's often better to derive signals. Solid's documentation states: What can be derived, should be derived.

In the component constructor you can see that we pass the time interval as frequency component property.

// App.tsx

import type { Component } from 'solid-js'
import { Time, Interval } from './Time'
import Wisdom from './Wisdom'

const App: Component = () => {
return (
<div>
<Time frequency={Interval.FiveSec} />
<Wisdom />
</div>

)
}

export default App

Props are a little confusing to me. It's not clear if I can set default props. Solid is sensitive when it comes to how you must handle the props. Because of how its reactivity primitives work you are not allowed to destructure props and I feel this can lead to a lot of frustration and time spent debugging.

Wisdom.tsxpermalink

This component is the core of the app. It's inside it where quote handling is done. Here it is in all its glory.

// Wisdom.tsx

import { createResource, Switch, Match } from 'solid-js'
import { Transition } from 'solid-transition-group'
import usa from './assets/usa.svg'

type Quote = {
quote: string
}

// this is defined in .env file in the project root folder
const KANYE_API = import.meta.env.VITE_KANYE_API as string

const fetchQuote = async () => (await fetch(KANYE_API)).json()

// Solid primitive to handle async resources
const [quote, { refetch }] = createResource<Quote>(fetchQuote)

export default function Wisdom() {
return (
<div class='container p-5 mx-auto max-w-3xl lg:mt-24'>
<h1 class='text-5xl md:text-7xl font-black text-indigo-900 lg:items-center lg:flex'>
<img src={usa} alt='USA' class='inline-block w-32 h-20' />
<div class='leading-none mt-5 lg:mt-0 lg:ml-4'>
Sh<span class='text-red-700'>*</span>t Kanye says
</div>
</h1>

<div class='mt-5 text-5xl md:text-7xl font-extrabold leading-none text-indigo-800'>
<Transition name='fade'>
<Switch fallback={<p>Failed to fetch quote</p>}>
<Match when={quote.loading}>
<p class="releative">Loading ...</p>
</Match>
{/* if you don't handle errors the whole app breaks */}
<Match when={quote.error}>
<p class="relative text-red-600">{quote.error.message}</p>
</Match>
<Match when={quote()}>{q => <p class="relative">{q.quote}</p>}</Match>
</Switch>
</Transition>
</div>

<div class='mt-10'>
{/* no way to easy style components with Tailwind */}
<button onClick={refetch} class='btn-fetch'>
Preach to me!
</button>
</div>
</div>

)
}

There are some Solid specific directives such as createResource, refresh, Switch, Match and Transition. Don't worry if they don't make sense. We will go through them later.

Styling components with WindiCSSpermalink

Styling in JSX works just as you expect, but unlike in React you can use normal class instead of className property name. As a Svelte developer I am spoiled by having scoped components styles in Svelte. Solid has an official style library - solid-styled-components. It's a css-in-js style library. For some reason I find it a bit inelegant to work with. I still gave it a try, but couldn't get it to work with Tailwind's @apply directive so I extracted the button's style to the global CSS stylesheet.

SVG Asset handlingpermalink

SVG handling is not something framework specific, but bundler specific. Because this starter project is Vite-based Vite takes perfect care of that for me. I wanted to import and render raw SVG file markup. If you use Vite you can append ?raw to your SVG imports to get the raw markup. I couldn't get it to work in Solid. so I used an image tag instead. Maybe there is a way to do it, but I was short on time.

import usa from './assets/usa.svg'

<img src={usa} alt='USA' class='inline-block w-32 h-20' />

Rendering raw SVG files is a breeze in a Svelte setup by using the @html directive.

Ajax Requestspermalink

Solid has a neat function called createResource that help you work with async functions. It's not reactive by default. The way it runs is by reacting to changes in the id supplied as a function argument. I don't have an id because I am fetching random quotes. Luckily there is a refetch function that lets you trigger fetching manually.

// Wisdom.tsx

import { createResource, Switch, Match } from 'solid-js'

type Quote = {
quote: string
}

// this is defined in .env file in the project root folder
const KANYE_API = import.meta.env.VITE_KANYE_API as string

const fetchQuote = async () => (await fetch(KANYE_API)).json()

const [quote, { refetch }] = createResource<Quote>(fetchQuote)

It's nice that you can supply a type to the function. Autocompletion in editor works out of the box, but the bundler doesn't catch a mistyped property name in JSX and gives you no error in dev console nor in the terminal.

Solid comes with many helper components that lets you handle common template control flows. There is <For> <Show>, <Suspense> and others. In this app I used <Switch>/<Match> component to display different quote states. This is an improvement over React where it's common practice to use raw JS code to handle these cases.

// Wisdom.tsx

<Switch fallback={<p>Failed to fetch quote</p>}>
<Match when={quote.loading}>
<p>Loading ...</p>
</Match>

{/* if you don't handle errors the whole app breaks */}
<Match when={quote.error}>
<p class="text-red-600">{quote.error}</p>
</Match>
<Match when={quote()}>{q => <p class="relative">{q.quote}</p>}</Match>
</Switch>

One weird error I encountered is that if you get an error in the fetchQuote function it breaks the whole app unless you handle it. This happened to me when I got disconnected from the WIFI and couldn't fetch a quote. So it looks like you have to handle the error cases explicitly.

You should always handle error cases, but should the timer in a separate component be affected by a network error? I am not sure. In my opinion the components should be isolated, but apparently they are not in Solid.

CSS animationspermalink

This was an interesting problem. I wanted to implement a simple CSS fade transition for switching between quotes. In Svelte I would have used the #key block together with built-in Svelte transitions for that and be done, but in Solid I had to import a separate transition package for it - solid-transition-group.

There are multiple ways to build CSS transitions using this official library. The official example already had a fade transition so I just copied all the code and CSS classes from it.

// index.css

.fade-enter-active {
transition: opacity 200ms ease-in-out;
}
.fade-exit-active {
transition: opacity 200ms ease-in-out;
}
.fade-enter, .fade-exit-to {
opacity: 0;
}

The transition package has a known limitation. Transition and Transition Group work on detecting changes on DOM children. Therefore it only supports single DOM childs and not text or fragments. Because of that I had to wrap all the elements in paragraph tags.

In the code snippet below name='fade' is used to generate transition class names.

// Wisdom.tsx
import { Transition } from 'solid-transition-group'

<Transition name='fade'>
<Switch fallback={<p>Failed to fetch quote</p>}>
<Match when={quote.loading}>
<p class="releative">Loading ...</p>
</Match>
{/* if you don't handle errors the whole app breaks */}
<Match when={quote.error}>
<p class="relative text-red-600">{quote.error.message}</p>
</Match>
<Match when={quote()}>{q => <p class="relative">{q.quote}</p>}</Match>
</Switch>

</Transition>

The implemented transition is a flaky experience. Sometimes it works, sometimes doesn't. Sometimes it works and suddenly it just stops. Maybe I did something wrong. I also feel that the whole thing is overly complex. Working with CSS transitions is so much easier in Svelte, because they are built-in.

Building the bundlepermalink

Solid has a really small runtime, 6 kB minified and gzipped which takes 1ms to download on a decent connection. It's five times smaller compared to full jQuery library, which is 30.4 kB.

The whole production build is around 10 kb. It's actually more or less on par with the bundle size of the Svelte version.

vite v2.4.2 building for production...
✓ 274 modules transformed.
dist/assets/usa.ede8af9e.svg 0.88kb
dist/index.html 0.55kb
dist/assets/index.9f119c76.js 2.11kb / brotli: 0.92kb
dist/assets/index.2c586559.css 4.85kb / brotli: 1.27kb
dist/assets/vendor.ce7411c6.js 31.43kb / brotli: 8.68kb

Impressive! Size does matter.

Codepermalink

Here is the complete app source code: https://github.com/codechips/solidjs-playground

Summarypermalink

Overall I am impressed by SolidJS. It's an advanced framework and code can get complex, but at the same time I think if you master it you can write fast and efficient code that can make your apps fly. Because of the complexity I wouldn't recommend it as your first framework.

To me personally SolidJS is a next step up for the advanced Svelte developer who wants to try something new, wants to have one-way bindings as default and fine-grained reactivity. Or a React developer who likes JSX, but who is burned out from React.

However, I wouldn't recommend Solid just yet if your app needs advanced CSS animations. It's doable, but in Svelte you get it our of the box and it's simpler to do too. I also couldn't figure out how to use framework agnostic JS libraries, such as tom-select for example, something that is easy to do in Svelte.

SolidJS is still a young framework. This often means that there are not many third-party libraries for it yet. Even though it's primarily based on JSX and feels React-ish you cannot re-use any of the existing React libraries.

Although Solid has support for SSR there is no SSR framework, ala Next.js or SvelteKit for it yet. I am exited to see what community will build on top it. I cross my fingers and hope for the best, because Solid is a great and promising UI library.


If you liked what you read, it would make me really happy if you could share it on Twitter.