SolidJS Tutorial: Learn Fine-Grained Reactivity from Scratch (2026)
SolidJS is a declarative JavaScript framework for building user interfaces with fine-grained reactivity. Created by Ryan Carniato, SolidJS compiles JSX templates into real DOM nodes and updates them with surgical precision. Unlike React’s virtual DOM diffing, SolidJS tracks dependencies at the signal level, updating only the exact DOM nodes that depend on changed state.
SolidJS consistently benchmarks as one of the fastest JavaScript UI frameworks, often matching vanilla JS performance. Its reactivity system is inspired by Knockout.js and MobX, but compiled away at build time for minimal overhead. SolidJS offers React-like JSX syntax with a fundamentally different reactivity model.
Signals: The Reactivity Primitive
Signals are the foundation of SolidJS reactivity. A signal is a pair of getter and setter functions wrapping a value. When the getter is called inside a tracking context (like a computed or effect), the dependency is automatically recorded. When the setter updates the value, all tracked dependencies re-execute.
Unlike useState in React, signals are not tied to a component’s lifecycle — they exist independently. createSignal returns [getter, setter]. The getter is a function, not a raw value, enabling precise dependency tracking without a virtual DOM or hook rules.
import { createSignal, createEffect } from 'solid-js';
function Counter() {
const [count, setCount] = createSignal(0);
createEffect(() => {
console.log(`Count is: ${count()}`);
});
return (
Count: {count()}
);
}
// Signals outside components
const [todos, setTodos] = createSignal([]);
// Computed values
const completedCount = createMemo(() =>
todos().filter(t => t.completed).length
);
Computed Values and Memos
createMemo creates a derived signal that only recalculates when its dependencies change. Memos are lazily evaluated and cache their result. Unlike createEffect, memos are pure — they return a value rather than produce side effects.
Memos automatically track dependencies. If a memo’s dependencies haven’t changed, reading the memo returns the cached value without re-executing the computation. This is critical for expensive operations like filtering lists or transforming data.
import { createSignal, createMemo } from 'solid-js';
const [items, setItems] = createSignal([
{ text: 'Learn SolidJS', done: true },
{ text: 'Build an app', done: false },
]);
const completedItems = createMemo(() =>
items().filter(item => item.done)
);
const progress = createMemo(() => {
const total = items().length;
const done = completedItems().length;
return total === 0 ? 0 : Math.round((done / total) * 100);
});
console.log(progress()); // 50
// Memo with multiple dependencies
const [filter, setFilter] = createSignal('all');
const filteredItems = createMemo(() => {
const currentFilter = filter();
const allItems = items();
switch (currentFilter) {
case 'active': return allItems.filter(i => !i.done);
case 'completed': return allItems.filter(i => i.done);
default: return allItems;
}
});
JSX Compilation and DOM Updates
SolidJS compiles JSX into real DOM node creation and update statements. The compiler analyzes each JSX expression and wraps reactive parts in update functions. When a signal changes, only the specific DOM element with that binding is updated — not the entire component tree.
This compilation approach eliminates the virtual DOM overhead while maintaining a declarative developer experience. Control flow components like
// Template compilation example
// This JSX:
function App() {
const [name, setName] = createSignal('World');
return Hello {name()}!
;
}
// Compiles approximately to:
function App() {
const [name, setName] = createSignal('World');
const h1 = document.createElement('h1');
// Fine-grained update: only this text node updates
const textNode = document.createTextNode('Hello ');
const nameNode = document.createTextNode(name());
const endNode = document.createTextNode('!');
h1.append(textNode, nameNode, endNode);
createEffect(() => { nameNode.data = name(); });
return h1;
}
// Control flow compiles to direct DOM ops
{(item) => {item.name} }
Resources and Async Data
createResource manages asynchronous data fetching with built-in loading, error, and refetching states. It wraps any async function and returns a signal-like resource with reactive updates. Resources integrate with Suspense for coordinated loading states.
The resource’s source signal triggers refetches when changed. Resources support server-side rendering with hydration, streaming, and caching strategies. The mutate function directly updates the resource cache without refetching.
import { createResource, Suspense } from 'solid-js';
async function fetchUser(id) {
const res = await fetch(`/api/users/${id}`);
if (!res.ok) throw new Error('Failed to fetch');
return res.json();
}
function UserProfile(props) {
const [user, { mutate, refetch }] = createResource(
() => props.id,
fetchUser
);
return (
Loading... {user()?.name}
{user()?.email}