MinhVo

Minh Vo

rss feed

Slaying code & making it lit fr fr πŸ”₯ tagline

Hey there πŸ‘‹ I'm an AI Engineer with 7 years of experience building scalable web and mobile applications. Currently at Neurond AI (May 2025 β€” present), architecting an Enterprise AI Assistant Platform with multi-tenant RAG on pgvector, multi-provider LLM orchestration, and Azure-native infrastructure. Previously spent 5+ years at SNAPTEC (Sep 2019 β€” Apr 2025), leading SaaS themes, admin dashboards, and e-commerce platforms β€” earned the Hero of the Year award in 2021. I specialize in TypeScript, React, Next.js, and AI-Native engineering with Claude Code and Cursor.bio

Back to blogs

SolidJS: Fine-Grained Reactivity for the Web

Explore SolidJS: signals, effects, components, and how fine-grained reactivity eliminates VDOM.

SolidJSFrontendJavaScriptReactivity

By MinhVo

Introduction

SolidJS has earned a unique position in the JavaScript framework landscape: it uses JSX like React, has a composition model like React, but delivers performance that rivals vanilla JavaScript. The secret is its fine-grained reactivity system β€” Solid never re-renders components. Instead, it compiles JSX into direct DOM operations and uses Signals to surgically update only the parts of the DOM that change.

This is not an incremental improvement over virtual DOM diffing. Benchmarks consistently show Solid.js matching or beating hand-optimized vanilla JavaScript on update-heavy workloads, while providing the developer ergonomics of a component-based framework. For applications that handle real-time data, frequent animations, or large lists, Solid.js offers a fundamentally better performance profile.

Understanding Solid.js also teaches you how reactivity works at the deepest level. Its primitives β€” Signals, Effects, Memos, and Stores β€” are the same concepts now being adopted by Angular, Preact, and the TC39 standards process. Learning Solid.js is learning the future pattern of frontend development.

SolidJS Architecture

Understanding SolidJS: Core Concepts

Solid.js is built on a simple observation: the virtual DOM exists to solve a problem that shouldn't exist. React needs to re-render components and diff the virtual DOM because it doesn't know what changed. Solid knows exactly what changed because every piece of state is a Signal, and every DOM binding is a direct subscription to that Signal.

Signals

Signals are the fundamental reactive primitive. A Signal is a getter/setter pair. Reading the getter inside a tracking scope (an effect, memo, or JSX binding) creates a dependency. Writing to the setter notifies all dependents.

import { createSignal } from "solid-js";
 
const [count, setCount] = createSignal(0);
 
console.log(count()); // 0
setCount(5);
console.log(count()); // 5

The function call syntax count() is deliberate. It lets Solid's compiler transform JSX expressions into optimized tracking scopes, and it signals (pun intended) that reading this value has reactive consequences.

Effects

Effects run a function whenever their tracked dependencies change. They are the mechanism that connects reactive state to the DOM and other side effects.

import { createEffect } from "solid-js";
 
createEffect(() => {
  console.log(`Count changed to: ${count()}`);
});

Memos (Derived Values)

Memos are computed values that cache their result and only recompute when dependencies change. They are lazy β€” they only compute when read.

import { createMemo } from "solid-js";
 
const [items, setItems] = createSignal([1, 2, 3, 4, 5]);
const sum = createMemo(() => items().reduce((a, b) => a + b, 0));
 
console.log(sum()); // 15 β€” computed here, not before

Why Components Don't Re-Render

In Solid.js, a component function runs exactly once. After that initial execution, only the reactive expressions (Signal reads inside JSX) update. This means:

  • No virtual DOM diffing
  • No unnecessary component function re-executions
  • No need for React.memo or useMemo optimizations
  • State can be defined outside components without hook rules
function Counter() {
  // This log appears exactly ONCE, no matter how many times count changes
  console.log("Counter component body executed");
 
  const [count, setCount] = createSignal(0);
 
  return (
    <button onClick={() => setCount(c => c + 1)}>
      Count: {count()} {/* Only this text node updates */}
    </button>
  );
}

Architecture and Design Patterns

Compilation Model

Solid.js compiles JSX at build time into optimized DOM creation and update code. The compiler transforms high-level JSX into low-level DOM API calls with reactive bindings.

// Source JSX
<div>{count()}</div>
 
// Compiled output (simplified)
const _el$ = _tmpl$.cloneNode(true);
createEffect(() => _el$.textContent = count());

This compilation approach means Solid.js has near-zero runtime overhead for the reactive system. The framework runtime is approximately 7KB gzipped β€” smaller than React (42KB) and even Preact (3KB) when you include the Signals add-on.

Component Structure

import { createSignal, createEffect, createMemo, Show, For, Switch, Match } from "solid-js";
 
interface UserProps {
  userId: string;
}
 
function UserProfile(props: UserProps) {
  const [user, setUser] = createSignal(null);
  const [loading, setLoading] = createSignal(true);
 
  createEffect(async () => {
    setLoading(true);
    const response = await fetch(`/api/users/${props.userId}`);
    setUser(await response.json());
    setLoading(false);
  });
 
  const displayName = createMemo(() =>
    user() ? `${user().firstName} ${user().lastName}` : "Loading..."
  );
 
  return (
    <Show when={!loading()} fallback={<div>Loading profile...</div>}>
      <div>
        <h2>{displayName()}</h2>
        <p>{user().email}</p>
        <For each={user().posts}>
          {(post) => <article><h3>{post.title}</h3><p>{post.body}</p></article>}
        </For>
      </div>
    </Show>
  );
}

Control Flow Components

Solid.js replaces the virtual DOM's implicit control flow with explicit components that manage their own DOM:

  • <Show> β€” conditionally renders content
  • <For> β€” renders lists with keyed reconciliation
  • <Switch> / <Match> β€” multi-way conditional rendering
  • <Suspense> β€” async boundary for streaming
function TodoList() {
  const [todos, setTodos] = createSignal([
    { id: 1, text: "Learn Solid.js", done: false },
    { id: 2, text: "Build an app", done: false },
  ]);
 
  return (
    <ul>
      <For each={todos()}>
        {(todo) => (
          <li>
            <input
              type="checkbox"
              checked={todo.done}
              onChange={() =>
                setTodos((t) =>
                  t.map((item) =>
                    item.id === todo.id ? { ...item, done: !item.done } : item
                  )
                )
              }
            />
            <span style={{ "text-decoration": todo.done ? "line-through" : "none" }}>
              {todo.text}
            </span>
          </li>
        )}
      </For>
    </ul>
  );
}

Step-by-Step Implementation

Project Setup

npx degit solidjs/templates/ts my-solid-app
cd my-solid-app
npm install
npm run dev

Building a Data Table with Signals

A data table with sorting, filtering, and pagination demonstrates Solid.js's reactivity model:

import { createSignal, createMemo, For } from "solid-js";
 
interface DataRow {
  id: number;
  name: string;
  email: string;
  role: string;
  status: "active" | "inactive";
}
 
function DataTable(props: { data: DataRow[] }) {
  const [sortKey, setSortKey] = createSignal<keyof DataRow>("name");
  const [sortDir, setSortDir] = createSignal<"asc" | "desc">("asc");
  const [filter, setFilter] = createSignal("");
  const [page, setPage] = createSignal(0);
  const pageSize = 10;
 
  const filtered = createMemo(() => {
    const q = filter().toLowerCase();
    if (!q) return props.data;
    return props.data.filter(
      (row) =>
        row.name.toLowerCase().includes(q) ||
        row.email.toLowerCase().includes(q) ||
        row.role.toLowerCase().includes(q)
    );
  });
 
  const sorted = createMemo(() => {
    const key = sortKey();
    const dir = sortDir();
    return [...filtered()].sort((a, b) => {
      const cmp = String(a[key]).localeCompare(String(b[key]));
      return dir === "asc" ? cmp : -cmp;
    });
  });
 
  const paged = createMemo(() => {
    const start = page() * pageSize;
    return sorted().slice(start, start + pageSize);
  });
 
  const totalPages = createMemo(() => Math.ceil(filtered().length / pageSize));
 
  function handleSort(key: keyof DataRow) {
    if (sortKey() === key) {
      setSortDir((d) => (d === "asc" ? "desc" : "asc"));
    } else {
      setSortKey(key);
      setSortDir("asc");
    }
    setPage(0);
  }
 
  return (
    <div>
      <input
        placeholder="Filter..."
        value={filter()}
        onInput={(e) => { setFilter(e.currentTarget.value); setPage(0); }}
      />
 
      <table>
        <thead>
          <tr>
            <For each={["name", "email", "role", "status"] as const}>
              {(key) => (
                <th onClick={() => handleSort(key)} style={{ cursor: "pointer" }}>
                  {key} {sortKey() === key ? (sortDir() === "asc" ? "↑" : "↓") : ""}
                </th>
              )}
            </For>
          </tr>
        </thead>
        <tbody>
          <For each={paged()}>
            {(row) => (
              <tr>
                <td>{row.name}</td>
                <td>{row.email}</td>
                <td>{row.role}</td>
                <td>
                  <span class={`badge ${row.status}`}>{row.status}</span>
                </td>
              </tr>
            )}
          </For>
        </tbody>
      </table>
 
      <div>
        <button disabled={page() === 0} onClick={() => setPage((p) => p - 1)}>
          Previous
        </button>
        <span>Page {page() + 1} of {totalPages()}</span>
        <button disabled={page() >= totalPages() - 1} onClick={() => setPage((p) => p + 1)}>
          Next
        </button>
      </div>
    </div>
  );
}

Fine-grained DOM updates

Real-World Use Cases

Use Case 1: Real-Time Trading Dashboard

Solid.js's update performance makes it ideal for applications displaying rapidly changing data. A trading dashboard updating hundreds of price cells per second with zero frame drops is a natural fit for fine-grained reactivity.

Use Case 2: Interactive Data Visualization

Charts and graphs that respond to hover, click, and zoom interactions need sub-millisecond state updates to maintain 60fps. Solid's ability to update individual visual elements without touching the rest of the DOM tree delivers smooth interactions even with thousands of data points.

Use Case 3: Form-Heavy Enterprise Applications

Complex forms with dozens of interdependent fields β€” where changing a dropdown filters options in three other selects and recalculates totals β€” are expressible as clean Signal graphs in Solid.js, without the re-render cascades that plague React implementations.

Use Case 4: Offline-First Applications

Solid's small runtime (7KB gzipped) and built-in Suspense support for async data make it excellent for Progressive Web Apps where bundle size and runtime performance are critical.

Best Practices for Production

  1. Destructure props with caution: Solid.js props are reactive getters. Destructuring them breaks reactivity. Use props.property access, not const { property } = props.

  2. Use <For> for lists, not .map(): The <For> component performs keyed reconciliation that tracks list items by identity. .map() creates new DOM elements on every change.

  3. Use <Show> for conditionals, not ternaries: <Show> preserves DOM state when toggling, while ternaries destroy and recreate DOM nodes.

  4. Keep Signals outside components when sharing: Unlike React hooks, Solid Signals don't need to live inside components. Define shared state at module scope for clean architecture.

  5. Use Stores for nested reactive state: For complex objects with many nested properties, createStore provides proxy-based deep reactivity that is more ergonomic than individual Signals.

  6. Profile with Solid DevTools: The Solid DevTools Chrome extension visualizes the Signal dependency graph, showing exactly which Signals trigger which DOM updates.

  7. Use untrack() to break dependency cycles: When reading a Signal inside an effect should not create a dependency, wrap it in untrack().

  8. Leverage batch() for multi-Signal updates: Batch multiple setter calls to prevent intermediate effect executions.

Common Pitfalls and Solutions

PitfallImpactSolution
Destructuring propsLost reactivity, stale valuesAlways access props.property directly
Using .map() for listsEntire list rebuilds on changeUse <For> component with key function
Creating Signals in JSXNew Signal on every renderCreate in component body, not in JSX return
Forgetting () on Signal readReturns the Signal function, not the valueAlways call signal() as a function
Overusing effects for derived stateUnnecessary side effects, stale dataUse createMemo for derived values
Not handling cleanup in effectsMemory leaks from subscriptionsReturn a cleanup function from effects

Performance Optimization

import { createSignal, createEffect, untrack, batch } from "solid-js";
 
// Untracked reads: read a Signal without creating a dependency
createEffect(() => {
  const primary = primarySignal();
  const secondary = untrack(() => secondarySignal());
  // This effect only re-runs when primarySignal changes
});
 
// Batch updates: prevent intermediate effect runs
function updateFilters(category: string, search: string, sort: string) {
  batch(() => {
    setCategory(category);
    setSearch(search);
    setSort(sort);
  });
  // Effects run once after all three Signals are updated
}
 
// Memoization for expensive computations
const expensiveResult = createMemo(() => {
  // Only runs when rawData() changes AND expensiveResult is read
  return rawData().map(transform).filter(validate).sort(compare);
});

Solid.js consistently ranks among the top 3 frameworks in the js-framework-benchmark, matching vanilla JavaScript on most metrics and significantly outperforming React, Vue, and Angular.

Comparison with Alternatives

FeatureSolid.jsReactVueSvelte
Re-renderingNeverEvery state changeReactive componentsReactive declarations
Virtual DOMNoYesYes (with reactive deps)No (compiled)
Bundle size~7KB~42KB~33KB~2KB
Update performanceExcellentGoodGoodExcellent
JSX supportYes (native)YesVia pluginNo
Learning curveLow–MediumLowLowLow
Server componentsSolidStartReact Server ComponentsNuxtSvelteKit
Reactivity modelSignalsHooks (manual)Proxy-basedCompiler
Ecosystem sizeGrowingLargestLargeMedium

Advanced Patterns

Context and Dependency Injection

import { createContext, useContext } from "solid-js";
 
const ThemeContext = createContext<"light" | "dark">("light");
 
function App() {
  return (
    <ThemeContext.Provider value="dark">
      <ChildComponent />
    </ThemeContext.Provider>
  );
}
 
function ChildComponent() {
  const theme = useContext(ThemeContext);
  return <div class={`theme-${theme}`}>Current theme: {theme}</div>;
}

Resource Management with Suspense

import { createResource, Suspense, ErrorBoundary } from "solid-js";
 
async function fetchUser(id: string) {
  const response = await fetch(`/api/users/${id}`);
  return response.json();
}
 
function UserPage(props: { userId: string }) {
  const [user] = createResource(() => props.userId, fetchUser);
 
  return (
    <ErrorBoundary fallback={<div>Error loading user</div>}>
      <Suspense fallback={<div>Loading...</div>}>
        <div>
          <h1>{user()?.name}</h1>
          <p>{user()?.email}</p>
        </div>
      </Suspense>
    </ErrorBoundary>
  );
}

Testing Strategies

import { describe, it, expect } from "vitest";
import { createSignal, createMemo, createEffect } from "solid-js";
 
describe("Solid.js reactivity", () => {
  it("signals propagate to effects", () => {
    const [count, setCount] = createSignal(0);
    let last = -1;
    createEffect(() => { last = count(); });
    expect(last).toBe(0);
    setCount(42);
    expect(last).toBe(42);
  });
 
  it("memos cache until dependencies change", () => {
    const [a, setA] = createSignal(1);
    let computeCount = 0;
    const derived = createMemo(() => { computeCount++; return a() * 2; });
 
    derived();
    derived();
    expect(computeCount).toBe(1);
 
    setA(2);
    derived();
    expect(computeCount).toBe(2);
  });
 
  it("batch prevents intermediate effect runs", () => {
    const { batch } = require("solid-js");
    const [a, setA] = createSignal(0);
    const [b, setB] = createSignal(0);
    let runs = 0;
    createEffect(() => { a(); b(); runs++; });
    expect(runs).toBe(1);
    batch(() => { setA(1); setB(1); });
    expect(runs).toBe(2); // 1 initial + 1 batched update
  });
});

SolidJS vs React: Mental Model Comparison

The fundamental difference between SolidJS and React is that React re-executes the entire component function on every state change while SolidJS executes the component function only once and surgically updates the DOM through reactive subscriptions. In React, the component function is the render function β€” it runs again to produce a new virtual DOM that React diffs against the previous one. In SolidJS, the component function is the setup function β€” it runs once to create reactive subscriptions that automatically update specific DOM nodes when their dependencies change.

This difference has profound implications for performance and developer mental model. In React, you must use useMemo and useCallback to prevent expensive computations and function recreations from triggering unnecessary re-renders. In SolidJS, these optimizations are unnecessary because the reactivity system only updates the specific computations that depend on changed values. The compiler and runtime handle the optimization that React developers must do manually.

The JSX compilation differs significantly between the two frameworks. React JSX compiles to React.createElement calls that produce virtual DOM nodes. SolidJS JSX compiles to direct DOM operations like createElement, insert, and createEffect that create real DOM nodes with reactive bindings. This means SolidJS JSX is not a templating language that gets diffed β€” it's a compilation target that produces optimized imperative DOM manipulation code.

Server-Side Rendering with SolidStart

SolidStart provides file-based routing and server-side rendering for SolidJS applications, similar to how Next.js serves React. The framework renders components to HTML on the server and hydrates them on the client, but the hydration process is fundamentally different from React's. Instead of attaching event listeners to the entire component tree, SolidJS only hydrates the reactive bindings, which are a small subset of the total DOM nodes. This results in significantly faster hydration times, especially for content-heavy pages.

Server functions in SolidStart use the "use server" directive to define functions that execute only on the server. These functions can directly access databases, file systems, and environment variables without exposing sensitive code to the client bundle. The framework automatically generates the client-side RPC call and the server-side handler, providing type-safe server communication without manual API route definitions.

Streaming SSR in SolidStart sends HTML to the client as it's generated rather than waiting for the entire page to render. Combine this with Suspense boundaries to progressively load data-heavy sections while the initial HTML arrives immediately. The client receives a loading placeholder for pending sections that gets replaced with the actual content as the server streams it in. This pattern delivers the fastest possible time to first contentful paint.

Future Outlook

Solid.js and SolidStart continue to evolve. SolidStart 1.0 brings file-based routing, server functions, and streaming SSR β€” features comparable to Next.js but with Solid's fine-grained reactivity. The Solid team is also working on improved TypeScript support, better DevTools, and integration with the TC39 Signals proposal.

The broader impact of Solid.js extends beyond its own ecosystem. React Forget (the React compiler) aims to achieve similar automatic memoization through compilation. Angular's new Signal-based change detection is directly inspired by Solid's approach. Solid.js proved that fine-grained reactivity works at scale, and the rest of the ecosystem is following.

SolidJS vs React Mental Model

The key difference between SolidJS and React is that SolidJS components execute once to set up the reactive graph, while React components re-render whenever state changes. In React, calling useState creates state and the component function runs again on every update. In SolidJS, createSignal creates a reactive primitive and the component function never runs again β€” only the specific DOM elements that depend on the signal update. This means there is no need for useMemo, useCallback, or React.memo in SolidJS because there are no unnecessary re-renders to prevent. Understanding this fundamental difference is essential when transitioning from React to SolidJS.

Conclusion

Solid.js demonstrates that the virtual DOM is not necessary for a great component-based developer experience. By compiling JSX into direct DOM operations and using Signals for state management, Solid eliminates the performance overhead of diffing while providing automatic optimization that developers don't need to think about.

The key takeaways:

  1. Components run once β€” only reactive expressions (Signal reads in JSX) update
  2. Signals are function calls β€” count() creates a dependency, setCount() triggers updates
  3. Memos are lazy and cached β€” they compute only when read and only when dependencies change
  4. Effects bridge state to the DOM β€” they run when tracked dependencies change
  5. Control flow is explicit β€” <For>, <Show>, <Switch> manage DOM lifecycle
  6. Props are reactive getters β€” never destructure them, always access via props.property
  7. Performance is excellent β€” Solid consistently matches vanilla JavaScript benchmarks

Solid.js is not just a fast framework β€” it's a fundamentally different approach to building UIs that happens to be fast. Whether you adopt it for production or study it to understand reactivity, Solid.js is essential knowledge for modern frontend developers.