SolidJS Comparison
How Xote compares to SolidJS, especially where they share the same reactive model.
At a Glance
Overview
| Aspect | SolidJS | Xote |
|---|---|---|
| Reactive model | Fine-grained signals | Fine-grained signals |
| Component execution | Usually once | Usually once |
| Routing | Separate package | Built in |
| SSR | Strong framework story via SolidStart | Built-in primitives |
| Language | JavaScript / TypeScript | ReScript |
| Scope | Framework plus ecosystem | Smaller integrated UI library |
SolidJS and Xote are conceptually much closer to each other than either is to React. The real differences are language choice, what is included out of the box, and how explicit the UI bindings are.
Shared Ground
Shared Philosophy
Both libraries:
- use signals as the foundation
- avoid virtual DOM diffing for ordinary updates
- let components establish reactive structure once
- rely on dependency tracking instead of dependency arrays
If you already understand SolidJS, Xote's mental model will feel familiar quickly.
Runtime Model
Signals and State
SolidJS uses getter and setter pairs. Xote uses explicit read and write functions against a signal value.
1import { createSignal, createMemo, createEffect } from "solid-js";23const [count, setCount] = createSignal(0);4const doubled = createMemo(() => count() * 2);56createEffect(() => {7 console.log(count());8});
1open Xote23let count = Signal.make(0)4let doubled = Computed.make(() => Signal.get(count) * 2)56Effect.run(() => {7 Console.log(Signal.get(count))8 None9})
Key differences:
- SolidJS reads via function calls like
count(), Xote reads viaSignal.get - Xote effects return cleanup directly; SolidJS uses
onCleanup - Xote signals use strict equality by default and let you opt into custom equality with
~equals
Component Model
Both component models are close: the function runs, the DOM structure is created, and later updates flow through reactive bindings instead of through repeated component renders.
The main ergonomic difference is that SolidJS can embed reactive expressions directly inside JSX, while Xote makes the reactive boundary explicit with JSX primitives such as View.Int, View.Text, and View.For.
1function Counter() {2 const [count, setCount] = createSignal(0);34 return (5 <div>6 <h1>Count: {count()}</h1>7 <button onClick={() => setCount(c => c + 1)}>Increment</button>8 </div>9 );10}
1let counter = () => {2 let count = Signal.make(0)34 <div>5 <h1>6 <View.Text> "Count: " </View.Text>7 <View.Int> {count} </View.Int>8 </h1>9 <button onClick={_ => Signal.update(count, n => n + 1)}>10 <View.Text> "Increment" </View.Text>11 </button>12 </div>13}
List Rendering
SolidJS uses control-flow helpers like <For> and <Index>. Xote exposes the same idea through View.For, with an optional by prop for keyed reconciliation.
1import { For } from "solid-js";23function TodoList(props) {4 return (5 <ul>6 <For each={props.todos}>7 {todo => <li>{todo.text}</li>}8 </For>9 </ul>10 );11}
1let todoList = () => {2 let todos = Signal.make([{id: "1", text: "Buy milk"}])34 <ul>5 <View.For6 each={MaybeSignal.reactive(todos)}7 by={todo => todo.id}8 render={todo => <li> <View.Text> {todo.text} </View.Text> </li>}9 />10 </ul>11}
Both approaches preserve identity. Xote asks for the key function explicitly, which keeps the reconciliation contract visible in the code.
Platform Surface
Server-Side Rendering
SolidJS has the more complete application story through SolidStart. If you want file-based routing, streaming, and framework-level conventions, SolidStart is a major advantage.
Xote's SSR primitives are smaller and more manual. You render HTML, optionally serialize state with SSRState, and hydrate the result on the client.
1let html = SSR.renderDocument(2 ~scripts=["/client.js"],3 ~stateScript=SSRState.generateScript(),4 app,5)67Hydration.hydrateById(app, "root")
Routing
SolidJS uses @solidjs/router rather than shipping a router in the core package. Xote includes one. That difference matters more for smaller apps than for large framework-driven apps, but it is still an important scope distinction.
Runtime Footprint and Compilation
Both libraries produce small output. The practical difference is in the toolchain:
- SolidJS uses a JSX compiler that turns reactive expressions into direct DOM operations
- Xote relies on the ReScript compiler and generic JSX transform, then uses explicit reactive nodes where needed
SolidJS feels more implicit in JSX. Xote is more explicit about where reactivity lives.
Benchmarks
The repository ships a keyed-list benchmark that runs the same table application in Xote, React, Vue, and SolidJS. The two implementations here are structurally the same program: one signal holds the list, each row owns a signal for its label, and updates are batched.
Median of 15 iterations, Chromium 141 on a 4-core Xeon. Lower is better. These are ratios from one machine, not absolute performance claims, and re-running the same code moves individual ratios by 10-25% — read the close rows as ties.
| Operation | Xote | SolidJS |
|---|---|---|
| Create 1,000 rows | 88.7 ms | 52.5 ms |
| Replace 1,000 rows | 93.8 ms | 65.7 ms |
| Update every 10th row | 6.3 ms | 5.4 ms |
| Select a row | 1.0 ms | 0.8 ms |
| Swap two rows | 7.3 ms | 6.3 ms |
| Remove a row | 6.2 ms | 5.3 ms |
| Create 10,000 rows | 919 ms | 660 ms |
| Clear 10,000 rows | 109 ms | 62 ms |
| Time to first render | 24.4 ms | 22.8 ms |
| App bundle, gzipped | 8.4 KB | 4.7 KB |
Because both use the same reactive model, targeted updates land in the same range: updating scattered rows, selecting a row and startup are ties within the run-to-run spread, and both write single text nodes rather than diffing.
The gaps are in the parts SolidJS compiles. Its JSX compiler emits a template that is cloned per row, so building 1,000 rows costs about 2,000 DOM calls; Xote constructs each node through the view tree and spends closer to 20,000. That accounts for most of the difference on creation, and for the larger heap Xote holds at 10,000 rows.
Reordering used to be the one result that was not a constant-factor difference: Xote's keyed reconciler walked the new order against the live DOM and inserted every node whose position did not match, so a single early mismatch cascaded into 997 insertBefore calls against SolidJS's 2, and about 10x the time. Both now compute a minimal move set — the rows already in the right relative order are left alone — so a two-row swap costs two moves in either library and the times are a tie.
Type Safety
SolidJS with TypeScript is productive and familiar, but still lives in TypeScript's structural and partially unsound model.
Xote benefits from ReScript's stricter type guarantees. If that matters more to your team than staying in JS/TS syntax, it is a strong reason to prefer Xote.
Ecosystem
SolidJS has a meaningful and growing ecosystem, especially around SolidStart and headless UI work.
Xote is intentionally smaller. That means fewer ready-made packages, but also fewer architectural decisions outsourced to third-party dependencies.
Choosing Between Them
When to Choose SolidJS
- Reach for SolidJS when you want fine-grained reactivity while staying in JavaScript or TypeScript.
- Reach for SolidJS when the stronger ecosystem and framework story matter immediately.
- Reach for SolidJS when SolidStart and its conventions are part of the expected stack.
When to Choose Xote
- Reach for Xote when a smaller integrated API is more valuable than broader ecosystem depth.
- Reach for Xote when ReScript's type system and compilation model are part of the appeal.
- Reach for Xote when you prefer explicit reactive bindings over more automatic JSX transforms.
Migration Considerations
SolidJS developers typically need to adapt in three places:
- signal reads move from function calls to
Signal.get - reactive JSX expressions often become
View.Text,View.Int,View.For, or another explicitViewprimitive - routing and SSR move from the Solid ecosystem to Xote's built-in modules
The underlying mental model stays largely the same.