Skip to main content
Ganesh Joshi
Back to Blogs

Solid.js: fine-grained reactivity without a virtual DOM

February 21, 20265 min read
Tips
Solid.js or reactive code on screen

Solid.js takes a different approach to UI: instead of a virtual DOM, it compiles JSX directly to DOM operations. Instead of re-rendering components, it uses fine-grained reactivity where only the specific expressions that depend on changed data update. The result is typically faster performance with a smaller runtime.

Core concepts

Solid is built on three reactive primitives:

Primitive Purpose
createSignal Reactive state
createEffect Side effects on change
createMemo Computed values

These form the foundation of Solid's reactivity system.

Signals

Signals hold reactive state. Unlike useState, they return a getter function and a setter:

import { createSignal } from 'solid-js';

function Counter() {
  const [count, setCount] = createSignal(0);

  return (
    <button onClick={() => setCount(count() + 1)}>
      Count: {count()}
    </button>
  );
}

Key differences from React:

  • count is a function—call it to read the value
  • The component function runs once, not on every update
  • Only {count()} updates when the signal changes

Fine-grained updates

In React, state changes re-render the component. In Solid, only reactive expressions re-run:

function Profile() {
  const [name, setName] = createSignal('Alice');
  const [age, setAge] = createSignal(30);

  console.log('Component runs once');

  return (
    <div>
      <p>Name: {name()}</p>  {/* Updates when name changes */}
      <p>Age: {age()}</p>     {/* Updates when age changes */}
    </div>
  );
}

The console log runs once. Changing name only updates the first paragraph's text node.

Effects

createEffect runs side effects when dependencies change:

import { createSignal, createEffect } from 'solid-js';

function Timer() {
  const [count, setCount] = createSignal(0);

  createEffect(() => {
    console.log('Count changed:', count());
    // Automatically tracks count() as a dependency
  });

  return <button onClick={() => setCount(c => c + 1)}>{count()}</button>;
}

No dependency array needed—Solid tracks what you read.

Cleanup

Return a cleanup function from effects:

createEffect(() => {
  const handler = () => console.log('scroll');
  window.addEventListener('scroll', handler);

  return () => window.removeEventListener('scroll', handler);
});

Memos

createMemo caches computed values:

import { createSignal, createMemo } from 'solid-js';

function FilteredList() {
  const [items, setItems] = createSignal([1, 2, 3, 4, 5]);
  const [filter, setFilter] = createSignal(2);

  const filtered = createMemo(() =>
    items().filter(item => item > filter())
  );

  return (
    <ul>
      <For each={filtered()}>
        {(item) => <li>{item}</li>}
      </For>
    </ul>
  );
}

The memo recalculates only when items or filter change.

Control flow

Solid uses components for control flow instead of JavaScript:

import { Show, For, Switch, Match } from 'solid-js';

function App() {
  const [show, setShow] = createSignal(true);
  const [items, setItems] = createSignal(['a', 'b', 'c']);
  const [status, setStatus] = createSignal('loading');

  return (
    <>
      {/* Conditional */}
      <Show when={show()} fallback={<p>Hidden</p>}>
        <p>Visible</p>
      </Show>

      {/* List */}
      <For each={items()}>
        {(item, index) => <li>{index()}: {item}</li>}
      </For>

      {/* Switch/case */}
      <Switch>
        <Match when={status() === 'loading'}>Loading...</Match>
        <Match when={status() === 'error'}>Error!</Match>
        <Match when={status() === 'success'}>Done!</Match>
      </Switch>
    </>
  );
}

These components enable fine-grained updates—items aren't recreated when the list changes.

Stores for complex state

For nested reactive data, use stores:

import { createStore } from 'solid-js/store';

function TodoApp() {
  const [state, setState] = createStore({
    todos: [
      { id: 1, text: 'Learn Solid', done: false },
      { id: 2, text: 'Build app', done: false },
    ],
  });

  const toggle = (id: number) => {
    setState('todos', todo => todo.id === id, 'done', done => !done);
  };

  return (
    <For each={state.todos}>
      {(todo) => (
        <div onClick={() => toggle(todo.id)}>
          {todo.text}: {todo.done ? '✓' : '○'}
        </div>
      )}
    </For>
  );
}

Stores provide path-based updates for nested data.

Comparison with React

Aspect React Solid
Rendering Virtual DOM diffing Direct DOM updates
State useState returns value createSignal returns getter
Re-renders Whole component Specific expressions
Hooks rules Order matters No rules
Runtime size ~40KB ~7KB
Performance Good with optimization Fast by default

JSX differences

Solid JSX looks like React but has differences:

// Solid JSX
function Button(props) {
  return (
    <button
      class={props.class}           // class, not className
      classList={{ active: props.active }} // dynamic classes
      style={{ color: props.color }}  // object required
      onClick={props.onClick}        // not onClickCapture
      ref={props.ref}                // ref as prop
    >
      {props.children}
    </button>
  );
}

Props are getters

Props in Solid are getters to preserve reactivity:

// DON'T destructure at top level
function Bad({ name }) {  // Loses reactivity!
  return <p>{name}</p>;
}

// DO access through props
function Good(props) {
  return <p>{props.name}</p>;
}

// OR use splitProps for partial destructuring
function Better(props) {
  const [local, rest] = splitProps(props, ['name']);
  return <p {...rest}>{local.name}</p>;
}

SolidStart

SolidStart is Solid's meta-framework (like Next.js for React):

// routes/index.tsx
import { createResource } from 'solid-js';

export default function Home() {
  const [data] = createResource(fetchData);

  return (
    <Show when={data()} fallback={<p>Loading...</p>}>
      <main>{data().title}</main>
    </Show>
  );
}

Features include file-based routing, SSR, and streaming.

When to use Solid

Use case Recommendation
Performance-critical UI Strong fit
Small bundle size needed Strong fit
Embedded widgets Strong fit
Large existing React codebase Likely not worth migrating
Team knows React only Learning curve to consider
Need React ecosystem Stick with React

Migration considerations

Moving from React to Solid requires:

  • Learning the reactivity model
  • Rewriting components (similar but different)
  • Finding Solid equivalents for React libraries
  • Adjusting mental model (no re-renders)

For greenfield projects, Solid is worth evaluating. For existing React apps, the migration cost is often not justified.

Summary

Solid.js provides fine-grained reactivity without a virtual DOM. Signals track state, effects handle side effects, and memos cache computations. Components run once; only reactive expressions update. The result is typically faster performance with smaller bundles. Consider Solid for new projects where performance matters and you're willing to learn a different reactivity model.

Frequently Asked Questions

Solid.js is a reactive JavaScript framework that compiles JSX to direct DOM operations. It uses signals for state, has no virtual DOM, and provides fine-grained reactivity where only the specific DOM nodes that depend on changed data update.

Solid looks like React (JSX, components) but works differently. React re-renders components; Solid runs components once and updates only reactive expressions. Solid is typically faster and has a smaller runtime.

Signals are reactive primitives. createSignal returns a getter and setter. Reading the getter subscribes to updates. When the setter is called, only code that read the signal re-runs—not the whole component.

Solid has primitives like createSignal, createEffect, and createMemo that serve similar purposes. Unlike React hooks, they don't have ordering rules and can be called conditionally or in loops.

Consider Solid for new projects where performance is critical or bundle size matters. For existing React projects with large codebases and ecosystem dependencies, the migration cost is significant.

Related Posts