solid
One week with Solid

This is turning into a series. Last July I spent a week with Vue, and three weeks ago a week with Svelte, which I finished more impressed than I expected, with SvelteKit as the likely starting point for my next project.
The replies to the Svelte post mostly said the same thing: if runes, the new reactivity model coming in Svelte 5, are what you liked, go and look at where they came from, which is Solid. So I gave it the same week and rebuilt the same thing for the third time - the admin page for the flat automation, with devices, rules and a form to edit one rule.
The usual disclaimer, once more: I am a backend developer, I like frameworks that make decisions for me, and I am not much of a CSS person. I have shipped Angular and React, and now written one small application each in Vue and Svelte.
This is Solid 1.8, with Vite 5 and Vitest 1.4, as of the end of March 2024.
Starting was a template, not a question
There is no create-solid wizard for a plain Solid application in the way there
is create-vue or create-svelte. The documented way in is to copy a template:
npx degit solidjs/templates/ts-vitest flat-admin
degit copies the files from the solidjs/templates repository, without the git
history. There are variants - JavaScript, TypeScript, with Vitest, with Tailwind,
and a few more. I picked TypeScript with Vitest, and got a Vite project with
vite-plugin-solid, a test already set up, and nothing else. No router, no data
layer and no deployment story until I added them.
That was the first difference from the previous two weeks. create-vue asked me
about the router and the store, and create-svelte gave me SvelteKit whether I
had thought about it or not. Solid gives you the view layer and lets you choose
the rest, which is closer to React than to either of them.
The full-stack framework, SolidStart, exists but is not at 1.0. It was rebuilt over the winter on a new foundation called Vinxi, which itself sits on Vite and Nitro, and its documentation still reads like a project in motion. I tried it on one evening, and I come back to it at the end.
npm run dev was Vite, the same dev server as the last two frameworks, and so
it was instant.
It looks like React and is nothing like React
The device list:
import { createMemo, createSignal, For } from 'solid-js'
import type { Device } from './types'
export function DeviceList(props: { devices: Device[] }) {
const [query, setQuery] = createSignal('')
const visible = createMemo(() =>
props.devices.filter((d) => d.name.toLowerCase().includes(query().toLowerCase())),
)
return (
<>
<input
value={query()}
onInput={(e) => setQuery(e.currentTarget.value)}
placeholder="Filter devices"
/>
<ul>
<For each={visible()}>
{(device) => <li classList={{ off: !device.on }}>{device.name}</li>}
</For>
</ul>
</>
)
}
A function that returns JSX, a hook-shaped createSignal, a memo: anyone who
has written React will read this without effort. That is intentional, and it is
also misleading, because the thing that runs is completely different.
In React, that function is called again on every render. In Solid, it is called
once. The JSX is compiled into code that creates the real DOM nodes a single
time, and then wires each signal directly to the specific text node or attribute
that reads it. When query changes, nothing re-renders. The memo reruns, For
adds or removes the list items that changed, and that is all. There is no virtual
DOM and nothing to diff.
The quickest way to feel this:
function Counter() {
const [count, setCount] = createSignal(0)
console.log('Counter ran')
return <button onClick={() => setCount(count() + 1)}>{count()}</button>
}
In React, that logs on every click. In Solid, it logs once, when the component
is created, and never again. Once I had seen that, several React habits simply
stopped applying. There are no dependency arrays anywhere, because signals track
their own readers - the same idea I liked in Vue and in the Svelte 5 preview -
and no useCallback or useMemo to keep references stable, because nothing is
being recreated.
Signals are functions, and I like that
A signal is a pair: a getter and a setter. You read query() and write
setQuery(...). The parentheses are Solid's .value, and I expected to dislike
them for the same reason.
I did not, and I think the reason is that the split between reading and writing
is doing real work. A component can be handed the getter and not the setter, and
then it can only read. The types say who is allowed to change the state, and
the code that changes it is always a call to something named set.... Since
a weekend with Rust last spring, "say who is allowed
to change this" is a question I ask everywhere, and Solid is the only one of the
five frameworks I have used that answers it in the signature.
The other thing that made signals click is that they are not tied to components.
createSignal and createMemo work in a plain .ts file. The rule store I
needed ended up as a module with a signal in it:
import { createResource, createSignal } from 'solid-js'
import { fetchRules, saveRule } from './api'
import type { Rule } from './types'
const [version, setVersion] = createSignal(0)
export const [rules] = createResource(version, () => fetchRules())
export async function save(rule: Rule) {
await saveRule(rule)
setVersion((v) => v + 1)
}
createResource is Solid's built-in for async data: it takes a source signal
and a fetcher, refetches when the source changes, and gives you loading and
error alongside the value. It works with <Suspense> too. It is less than
Pinia, and more than I got from plain Svelte - without SvelteKit's load, which
is the fair comparison.
The trap is the same one as Vue's
Props in Solid are reactive, and they are reactive because props is an object
whose properties are getters. So this, the most natural thing in the world to a
React developer, breaks:
// devices is read once, when the component runs, and never again
export function DeviceList({ devices }: { devices: Device[] }) {
Destructuring calls the getter immediately and keeps the value. The list renders
once and never updates. It is exactly the Vue trap I wrote about in July, and
Solid's answer is similar: don't destructure, or use splitProps and
mergeProps when you need to separate or default them.
The same thing happens with an early return. Because the component only runs
once, if (!props.devices) return <p>Loading</p> is evaluated one time and never
revisited. Conditional rendering goes through <Show>, and lists through
<For>:
<Show when={rules()} fallback={<p>Loading…</p>}>
{(list) => <RuleList rules={list()} />}
</Show>
These control-flow components are the least React-like part of Solid, and the
part that stopped feeling strange quickest. They are Svelte's {#if} and
{#each}, written as components.
TypeScript is just TypeScript
This is Solid's clearest advantage over the other two, and it is a big one for someone who writes TypeScript on the server.
Solid components are .tsx files. tsc understands them with a single
"jsxImportSource": "solid-js" in tsconfig.json, and that is the end of the
setup. There is no vue-tsc, no svelte-check, no language server that has to
teach TypeScript about a new file format. Types flow through props, children,
generics and control-flow callbacks the way they flow through any other
TypeScript code, and the type checker I run in CI is the same one I run on the
backend.
In the Vue article I said React has the advantage here because JSX is TypeScript. Solid has the same advantage, without React's rendering model attached to it.
For someone who avoids CSS
This is where Solid gave back most of what I liked about Svelte.
There are no scoped styles. class works (it is class, not className), and
classList toggles classes from an object, which is pleasant. For scoping I used
CSS Modules, which Vite supports out of the box: name the file
DeviceList.module.css, import it, and use styles.off. That works, but it is a
second file per component, and it is the React answer, not the Svelte one.
There are no built-in transitions either. The community package
solid-transition-group does the job, but it is the job of wrapping elements
and writing CSS classes for enter and exit states - which is exactly the part I
was glad Svelte did for me. The save confirmation on the Solid version of the
page just appears, with no fade.
Testing, which was Vitest again
The template came with Vitest configured and a test in place, so there was nothing to find in a GitHub issue this time. The component test, with Solid's Testing Library, is almost the same as the Svelte one:
import { render, screen } from '@solidjs/testing-library'
import userEvent from '@testing-library/user-event'
import { describe, expect, it } from 'vitest'
import { DeviceList } from './DeviceList'
describe('DeviceList', () => {
it('filters devices by name', async () => {
const devices = [
{ id: 'kitchen', name: 'Kitchen light', room: 'kitchen', on: true },
{ id: 'hallway', name: 'Hallway light', room: 'hallway', on: false },
]
render(() => <DeviceList devices={devices} />)
await userEvent.type(screen.getByPlaceholderText('Filter devices'), 'kitchen')
expect(screen.getByText('Kitchen light')).toBeTruthy()
expect(screen.queryByText('Hallway light')).toBeNull()
})
})
The difference is that render takes a function returning JSX rather than a
component and its props. The nicer part is that because signals do not need a
component, the logic can be tested without a DOM at all: create the signals
inside createRoot, set values, assert on the memo, dispose. That is closer to a
unit test of a backend service than anything I managed with the other two.
Vitest reached 1.4 this month. Across three frameworks I used one test runner and did not have to relearn a single configuration idea, which is the most convincing argument for the Vite ecosystem I could have asked for.
What I liked
The reactivity model: components that run once, signals that update exactly what reads them, no dependency arrays and no re-rendering. Of the five frameworks I have now used, this is the one I understand best from first principles, because there is so little of it.
Read and write are separate, and getter and setter as two values makes ownership visible in the types. I did not expect a front-end framework to remind me of the borrow checker, in a good way.
Plain TypeScript: .tsx files, tsc, nothing else. After vue-tsc and
svelte-check, that was a relief.
Signals work outside components. State as a module with signals in it is the cleanest shared-state story of the three weeks, and the easiest to test.
What I did not like
It looks like React, so I kept writing React. Destructuring props and early
returns both work once and then silently stop updating. .map over an array
keeps working, but recreates every row on each change, which is what <For> is
there to avoid.
Where a signal is read matters. const q = query() at the top of a component
is a snapshot, because the component only runs once; the same call inside the
JSX or a memo is a live binding. Both type-check, both render, and
only one updates. I got it wrong more than once, the way I got .value wrong in
July.
There is no styling story: no scoped styles, no transitions, so every piece of polish on the page was something I had to add myself. A CSS person might call that freedom; to me it was homework.
Assembling the stack was on me: router, data loading, forms and deployment were all choices. That is the React experience I was happy to get away from in the previous two weeks.
The good about the framework
The core is small, stable and fast. Solid reached 1.0 in 2021, and the primitives
I used - createSignal, createMemo, createEffect, createResource, stores -
have not changed shape since. It sits at or near the top of the usual
rendering benchmarks, and the reason is visible in the model rather than hidden
in an optimiser.
It has also been influential well beyond its size. Solid is where signals became
a mainstream front-end idea again, and the industry has followed: Preact added
signals in 2022, Angular made signal and computed stable in version 17 last
November, Vue has had the same model since 3.0, and Svelte 5's runes are
signals under a compiler. When I wrote three weeks ago that three frameworks were
converging on one model, this is the model they are converging on.
And it keeps TypeScript and JSX as they are, which means every existing tool that
understands .tsx - the type checker, the editor, the linters, the formatter -
works unchanged.
The bad about the framework
The ecosystem is the smallest of the three, smaller even than Svelte's. There are good pieces - Kobalte for accessible headless components, Solid ports of some popular UI kits - but for many needs the answer is to wrap a plain JavaScript library yourself, and for a non-CSS person there is less to lean on.
The full-stack story is not finished. SolidStart is pre-1.0 and was rebuilt
this winter. The router's data APIs have also been reworked recently. None of it
is bad, and some of it looks very good - server functions with "use server"
are an interesting design - but I would not start something I needed to
maintain on it today. SvelteKit, by comparison, is on its second stable major.
Fewer people know it than know Svelte, and answers, examples and people to hire are all scarcer.
The React resemblance lowers the cost of starting and raises the cost of the first week, because intuitions you bring with you are wrong in ways that do not throw errors.
Where I landed
Solid impressed me. Its reactivity model is the cleanest of the five frameworks I have now spent time with, its TypeScript story is the best, and it is the one that taught me the most about how the others work underneath. I understand Svelte 5's runes better for having used the thing they are modelled on.
But Svelte is still my go-to framework, and the week did not come close to changing that.
The reasons are the ones that matter to someone who spends most of their time on
the backend. SvelteKit gives me a stable, server-first framework - load
functions, form actions, a build that refuses to leak a private environment
variable - and with Solid I would be assembling that from pieces, or building on
SolidStart before it reaches 1.0. Svelte gives me scoped styles and transitions
for free, and Solid gives me homework. Svelte components were shorter. And the
part of Solid I liked best, signals, is exactly what Svelte 5 is adopting. When
it ships, I get Solid's model inside the framework I already preferred.
So the ranking after three of these weeks: Svelte, with SvelteKit, for my next project. Solid is the one I will keep an eye on, and the one I would recommend to anyone who wants to understand what fine-grained reactivity actually is. Vue is still a very good choice that I would not argue anyone out of.
And the one thing all three had in common was Vite and Vitest underneath. I said in July that Vite would be bigger than Vue. Three frameworks later, it is the only part of the stack I did not have to think about once.


