web-dev6 min read

SolidJS Tutorial: Learn Fine-Grained Reactivity from Scratch (2026)

SolidJS Tutorial: Learn Fine-Grained Reactivity from Scratch (2026)

Published:  |  Category: Web Dev  |  Reading time: ~15 min
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 , , and are compiled into optimized DOM manipulation rather than re-running entire components.

// 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}

    ); }

    Context, Stores, and Global State

    createContext provides dependency injection without prop drilling. Combined with createSignal or createStore, it shares state across the component tree. createStore creates deeply reactive state objects with nested tracking — setting a deeply nested property only triggers updates for components reading that specific path.

    Stores return a proxy that tracks property-level access. Unlike React’s Context + useReducer pattern, SolidJS Context consumers only re-render when their accessed properties change, not when any part of the store updates.

    import { createContext, useContext, createStore, createSignal } from 'solid-js';
    
    const TodoContext = createContext();
    
    function TodoProvider(props) {
      const [todos, setTodos] = createStore([]);
      const [filter, setFilter] = createSignal('all');
    
      const addTodo = (text) => setTodos(todos.length, { text, done: false });
      const toggleTodo = (index) => setTodos(index, 'done', done => !done);
    
      return (
        
          {props.children}
        
      );
    }
    
    function TodoItem(props) {
      const { todos, toggleTodo } = useContext(TodoContext);
      // Only re-renders when this specific todo changes
      return (
        
  • toggleTodo(props.index)} /> {todos[props.index].text}
  • ); }

    Effects, Lifecycle, and Cleanup

    createEffect runs a function whenever its tracked dependencies change. Unlike React’s useEffect, the effect function itself is the tracking scope — no dependency array needed. Effects are automatically disposed when the owning component unmounts. onCleanup registers teardown logic.

    SolidJS provides onMount for initialization logic, onCleanup for disposal, and createRoot for managing reactive roots outside components. The batch function groups multiple signal updates into a single notification, preventing intermediate renders.

    import { createSignal, createEffect, onMount, onCleanup, batch } from 'solid-js';
    
    function TimerDisplay() {
      const [seconds, setSeconds] = createSignal(0);
      let interval;
    
      onMount(() => {
        interval = setInterval(() => setSeconds(s => s + 1), 1000);
      });
    
      createEffect(() => {
        console.log(`Timer: ${seconds()}s`);
      });
    
      onCleanup(() => {
        clearInterval(interval);
      });
    
      return 
    Elapsed: {seconds()}s
    ; } // Batching multiple updates function handleSubmit(e) { e.preventDefault(); batch(() => { setSubmitted(true); setErrors({}); setCurrentPage(1); }); // Only one notification propagates }

    Frequently Asked Questions

    How does SolidJS compare to React?

    SolidJS uses fine-grained reactivity with no virtual DOM, resulting in faster updates and smaller bundle sizes. It offers React-like JSX but with signals instead of hooks state. SolidJS apps start faster and use less memory than equivalent React apps.

    Can I use SolidJS for existing React projects?

    SolidJS uses JSX but has different primitives. Porting requires rewriting components with signals, createMemo, and createEffect. The UI patterns transfer, but the reactivity model is fundamentally different.

    Does SolidJS support TypeScript?

    Yes. SolidJS has first-class TypeScript support. createSignal, createStore, createResource, and all primitives are fully typed. The JSX compiler respects TypeScript type annotations.

    How does SolidJS handle server-side rendering?

    SolidJS supports SSR with hydration through SolidStart (meta-framework) or direct SSR setup. The fine-grained reactivity model enables partial hydration, where only interactive islands are hydrated on the client.

    Originally published on Ayodhyyya. Last updated June 1, 2026.