Frontend

Solid.js: Building Reactive Web Apps

Mayur Dabhi
Mayur Dabhi
June 9, 2026
14 min read

The frontend JavaScript landscape never stands still — and just when React felt like the definitive answer, Solid.js arrived to challenge every assumption we had about how reactive UIs should work. Created by Ryan Carniato, Solid.js isn't just another React clone with a different name. It fundamentally reimagines the relationship between state, components, and the DOM — trading the virtual DOM for true fine-grained reactivity. The result is a library that feels familiar to React developers yet performs at a level that makes most other frameworks look sluggish. If you've ever wondered whether we're leaving performance on the table with the virtual DOM model, Solid.js has a compelling answer.

Why Solid.js?

Solid.js consistently ranks at the top of the js-framework-benchmark, outperforming React by up to 30x in certain DOM operations. Its production bundle is around 6.4 KB gzipped — compared to ~45 KB for React + ReactDOM. The ecosystem is growing rapidly with solid-router, SolidStart (a full-stack meta-framework), and first-class TypeScript support built in from the ground up.

What is Solid.js?

Solid.js is a declarative, component-based UI library for building web interfaces. At first glance, it looks almost identical to React — you write JSX, compose components, and manage state. But under the hood, the execution model is completely different in three critical ways:

This architecture was inspired by reactive programming libraries like MobX and Knockout.js, but applied to a modern component model with JSX compilation. Ryan Carniato spent years researching reactive systems before releasing Solid 1.0 in 2021, and the technical depth shows in every design decision.

Solid.js vs React: Key Differences

Before diving into code, it's worth mapping the conceptual differences between Solid.js and React clearly. Many React patterns translate directly, but the mental model shift is real — and understanding it upfront saves confusion later.

Feature Solid.js React
Virtual DOM No — compiles to direct DOM ops Yes — reconciles a virtual tree
Re-renders Never — only signal subscribers update Component function re-runs on state change
State primitive createSignal (getter/setter pair) useState (value + setter)
Derived state createMemo (auto-memoized) useMemo (manual deps array)
Side effects createEffect (auto-tracks deps) useEffect (manual deps array)
Bundle size ~6.4 KB gzipped ~45 KB gzipped (React + ReactDOM)
Performance Near-native DOM speed Good, but with VDOM overhead
Component model Runs once, reactive via signals Re-runs on every render cycle
Ecosystem maturity Growing (solid-router, SolidStart) Mature (Next.js, thousands of libs)

The dependency array in React's useEffect is a common source of bugs — developers forget dependencies, add stale closures, or over-specify them causing unnecessary re-runs. Solid's createEffect tracks dependencies automatically by observing which signals are read during execution. This eliminates an entire category of bugs.

Getting Started with Solid.js

Setting up a Solid.js project is fast. You can use the official Vite template or the newer npm create solid@latest CLI that powers SolidStart as well.

1

Install Node.js

Solid.js requires Node.js 16 or higher. Download from nodejs.org or use nvm to manage versions. Verify with node --version.

2

Create a New Solid App

Use the official Vite-based template with npx degit, or use the interactive npm create solid@latest CLI for a SolidStart project with routing included.

Terminal
# Option A: Vite + Solid template (recommended for beginners)
npx degit solidjs/templates/js my-solid-app

# Option B: TypeScript variant
npx degit solidjs/templates/ts my-solid-app

# Option C: SolidStart (full-stack meta-framework)
npm create solid@latest my-solid-app

# Navigate into the project
cd my-solid-app

# Install dependencies
npm install

# Start the dev server (hot module replacement included)
npm run dev
3

Install Dependencies and Run

After npm install, run npm run dev to start the Vite dev server on http://localhost:3000 (or 5173 for newer Vite). Solid uses Vite's HMR for instant feedback.

Here's what your project structure looks like after scaffolding:

Project Structure
my-solid-app/
├── src/
│   ├── App.jsx          # Root component
│   ├── App.module.css   # CSS Modules (scoped styles)
│   ├── index.jsx        # Entry point — mounts to #root
│   └── logo.svg
├── index.html           # Vite HTML entry
├── package.json
└── vite.config.js       # Vite config with Solid plugin

The entry point src/index.jsx uses Solid's render function — similar to React's ReactDOM.createRoot but simpler:

src/index.jsx
import { render } from 'solid-js/web';
import App from './App';

// Mount the root component to #root in index.html
render(() => <App />, document.getElementById('root'));

Signals: The Core of Solid Reactivity

Signals are the fundamental reactive primitive in Solid.js. A signal is a getter/setter pair — the getter is a function that returns the current value and registers the caller as a subscriber. When you call the setter to update the value, every subscriber is automatically re-run. This simple mechanism powers the entire reactivity system.

The key insight is that calling a signal as a function (count()) is how Solid tracks dependencies. Solid's compiler and runtime observe every signal access during reactive contexts (effects, memos, JSX expressions) and wire up subscriptions automatically. When the signal updates, only those subscribers are notified — not entire component trees.

Signal createSignal(0) count() / setCount() createMemo Derived / computed value (cached) JSX Expression <p>{count()}</p> createEffect Side effects (logs, fetch, subscriptions) DOM Update Only changed nodes updated — no diffing, no reconciliation Solid.js Reactive Dependency Graph

Signals propagate changes directly to their subscribers — memo, effects, and DOM nodes — with no intermediate diffing step.

A signal is created with createSignal(initialValue). It returns a tuple of [getter, setter]. Call the getter as a function to read the value — this is what registers the dependency.

import { createSignal } from 'solid-js';

function Counter() {
  // count is a getter function, setCount is a setter
  const [count, setCount] = createSignal(0);

  return (
    <div>
      {/* Calling count() inside JSX tracks the dependency */}
      <p>Count: {count()}</p>
      <button onClick={() => setCount(count() + 1)}>
        Increment
      </button>
      <button onClick={() => setCount(c => c - 1)}>
        Decrement (functional update)
      </button>
    </div>
  );
}

When setCount is called, only the DOM node showing {count()} updates — the component function does not re-run.

createMemo creates a derived signal whose value is automatically recalculated only when its dependencies change. Unlike React's useMemo, there is no dependency array to maintain.

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

function TemperatureConverter() {
  const [celsius, setCelsius] = createSignal(0);

  // createMemo auto-tracks that it depends on celsius()
  const fahrenheit = createMemo(() => celsius() * 9/5 + 32);
  const kelvin = createMemo(() => celsius() + 273.15);

  return (
    <div>
      <input
        type="number"
        value={celsius()}
        onInput={e => setCelsius(+e.target.value)}
      />
      <p>{celsius()}°C = {fahrenheit()}°F = {kelvin()}K</p>
    </div>
  );
}

createMemo is lazy and memoized — it only recomputes when celsius() changes, and caches the result for multiple consumers.

createEffect runs a side effect whenever its reactive dependencies change. It automatically tracks which signals are read during execution — no dependency array needed.

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

function UserTracker() {
  const [userId, setUserId] = createSignal(1);
  const [user, setUser] = createSignal(null);

  // createEffect auto-tracks userId() as a dependency
  createEffect(() => {
    // Runs immediately, then re-runs whenever userId() changes
    console.log('Fetching user:', userId());
    fetch(`https://api.example.com/users/${userId()}`)
      .then(r => r.json())
      .then(data => setUser(data));
  });

  return (
    <div>
      <button onClick={() => setUserId(id => id + 1)}>
        Next User
      </button>
      <p>{user()?.name ?? 'Loading...'}</p>
    </div>
  );
}

No cleanup function is needed for simple effects, but createEffect supports returning a cleanup function for subscriptions or timers.

Building Components in Solid.js

Solid.js components look like React function components — they accept props and return JSX. The crucial difference is that Solid component functions run exactly once. There is no concept of a "render cycle" for components. All reactive behavior happens through signals and effects set up during that single execution.

Don't Destructure Props!

In Solid, props are reactive objects. Never write const { name } = props — destructuring breaks reactivity because it captures the value at the time of destructuring, not the live signal. Always access props as props.name. Use splitProps from solid-js if you need to forward a subset of props.

src/components/Greeting.jsx — Props & Children
import { createSignal } from 'solid-js';

// Child component — props are reactive, access as props.name
function Greeting(props) {
  return (
    <div class="greeting">
      {/* props.name is reactive — updates when parent changes it */}
      <h2>Hello, {props.name}!</h2>
      <p>Role: {props.role ?? 'Developer'}</p>
      {/* props.children renders nested content */}
      <div class="content">{props.children}</div>
    </div>
  );
}

// Parent component — passes props and children
function App() {
  const [name, setName] = createSignal('Mayur');

  return (
    <div>
      <input
        value={name()}
        onInput={e => setName(e.target.value)}
        placeholder="Enter your name"
      />
      <Greeting name={name()} role="Full Stack Dev">
        {/* These become props.children in Greeting */}
        <p>Welcome to Solid.js!</p>
      </Greeting>
    </div>
  );
}

Using mergeProps and splitProps

When you need to handle default values or forward a subset of props, Solid provides mergeProps and splitProps — both of which preserve reactivity unlike plain object destructuring:

mergeProps & splitProps — Reactive-Safe Patterns
import { mergeProps, splitProps } from 'solid-js';

function Button(props) {
  // mergeProps provides reactive defaults
  const merged = mergeProps({ type: 'button', variant: 'primary' }, props);

  // splitProps separates local props from forwarded props
  const [local, rest] = splitProps(merged, ['variant', 'children']);

  return (
    <button
      {...rest}
      class={`btn btn-${local.variant}`}
    >
      {local.children}
    </button>
  );
}

Control Flow, Stores & Async Data

Because Solid components don't re-render, standard JavaScript control flow like if/else and map() used directly in JSX won't react to signal changes — they only run once during component setup. Solid provides built-in reactive control flow components that integrate with the signal system.

Conditional Rendering with <Show>

Show Component — Conditional Rendering
import { createSignal } from 'solid-js';
import { Show } from 'solid-js';

function LoginToggle() {
  const [isLoggedIn, setIsLoggedIn] = createSignal(false);

  return (
    <div>
      <Show
        when={isLoggedIn()}
        fallback={<button onClick={() => setIsLoggedIn(true)}>Log In</button>}
      >
        {/* Only mounted when isLoggedIn() is true */}
        <div>
          <p>Welcome back!</p>
          <button onClick={() => setIsLoggedIn(false)}>Log Out</button>
        </div>
      </Show>
    </div>
  );
}

List Rendering with <For>

For Component — Reactive List Rendering
import { createSignal } from 'solid-js';
import { For } from 'solid-js';

function TodoList() {
  const [todos, setTodos] = createSignal([
    { id: 1, text: 'Learn Solid.js', done: false },
    { id: 2, text: 'Build something reactive', done: false },
  ]);

  const addTodo = (text) => {
    setTodos(t => [...t, { id: Date.now(), text, done: false }]);
  };

  return (
    <ul>
      {/* For provides a key-tracked, reactive list */}
      <For each={todos()}>
        {(todo, index) => (
          <li>
            {index() + 1}. {todo.text}
          </li>
        )}
      </For>
    </ul>
  );
}

Complex State with createStore

For nested objects and arrays where you want fine-grained reactivity at the property level (not just replacing the entire object), Solid provides createStore. Stores use JavaScript Proxies to track property access, so only the specific property that changes triggers updates.

createStore — Nested Reactive State
import { createStore } from 'solid-js/store';

function UserProfile() {
  const [user, setUser] = createStore({
    name: 'Mayur Dabhi',
    skills: ['JavaScript', 'React', 'Solid.js'],
    address: {
      city: 'Mumbai',
      country: 'India',
    },
  });

  // Update a deeply nested property — only that node in the DOM updates
  const updateCity = (city) => setUser('address', 'city', city);

  // Append to a nested array
  const addSkill = (skill) =>
    setUser('skills', s => [...s, skill]);

  return (
    <div>
      <h2>{user.name}</h2>
      <p>City: {user.address.city}</p>
      <button onClick={() => updateCity('Bengaluru')}>
        Change City
      </button>
    </div>
  );
}

Async Data with createResource

createResource is Solid's built-in solution for async data fetching. It integrates with Solid's <Suspense> boundary and returns a reactive signal that represents the loading state and resolved value.

createResource — Async Data Fetching
import { createSignal, createResource, Suspense } from 'solid-js';

// Fetcher function — receives the source signal value
const fetchUser = async (id) => {
  const response = await fetch(`https://jsonplaceholder.typicode.com/users/${id}`);
  return response.json();
};

function UserCard() {
  const [userId, setUserId] = createSignal(1);

  // createResource re-fetches whenever userId() changes
  const [user] = createResource(userId, fetchUser);

  return (
    <div>
      <button onClick={() => setUserId(id => id + 1)}>
        Load Next User
      </button>

      {/* Suspense shows fallback while resource is loading */}
      <Suspense fallback={<p>Loading user...</p>}>
        <Show when={user()} fallback={<p>No user found</p>}>
          <div>
            <h2>{user().name}</h2>
            <p>{user().email}</p>
            <p>{user().company.name}</p>
          </div>
        </Show>
      </Suspense>
    </div>
  );
}
createStore user.name user.address.city setUser() JS Proxy Intercepts get/set Tracks fine-grained notify Subscribers DOM node: city createEffect createMemo Store Proxy — Fine-Grained Property Update Flow

createStore uses JavaScript Proxies to intercept property reads and writes, enabling property-level reactivity within nested objects.

Conclusion: Is Solid.js Right for You?

Solid.js represents a genuine advancement in frontend reactivity. By abandoning the virtual DOM and instead compiling to direct DOM operations powered by fine-grained signals, it achieves performance characteristics that were previously only possible with hand-tuned vanilla JavaScript. For applications where rendering performance is critical — data-heavy dashboards, real-time UIs, or low-bandwidth mobile experiences — Solid.js is an exceptional choice.

The learning curve is gentle for React developers. The JSX syntax, component model, and overall patterns are familiar. The main shift is in understanding that components run once and all reactivity flows through signals, memos, and effects — a model that is arguably more predictable and easier to reason about than React's re-render cycle once you internalize it.

Key Takeaways

  • No Virtual DOM = better performance — Solid compiles JSX to direct DOM operations, updating only the exact nodes that depend on changed signals.
  • Signals are the fundamental reactive primitivecreateSignal returns a getter/setter pair. Calling the getter as a function (count()) registers the dependency automatically.
  • Components run once, not on every update — unlike React, Solid component functions execute exactly once at mount time. Reactivity happens through the signal system, not component re-execution.
  • Never destructure props — always access as props.name to preserve reactivity. Use splitProps and mergeProps for safe prop manipulation.
  • Built-in control flow components — use <Show> for conditionals and <For> for lists to ensure reactive rendering without JavaScript re-execution.
  • Stores for complex reactive objectscreateStore provides property-level fine-grained reactivity for nested state structures using JavaScript Proxies.
  • Growing ecosystem — solid-router for client-side routing, SolidStart as a full-stack meta-framework (similar to Next.js), and first-class TypeScript support make it production-ready today.
"The future of the web is reactive. Solid.js shows us what that future looks like when you stop pretending the DOM is slow and start working with it instead of around it."
— Ryan Carniato, Creator of Solid.js

Whether you adopt Solid.js for a new project today or simply study its reactivity model to deepen your understanding of frontend systems, the concepts it embodies — fine-grained subscriptions, compilation-based optimization, single-execution components — are shaping the direction of the entire ecosystem. Vue's Vapor mode, React's compiler, and Svelte's compiled output are all moving in this direction. Solid.js just got there first.

Solid.js JavaScript Reactive Signals Frontend Performance
Mayur Dabhi

Mayur Dabhi

Full Stack Developer with 5+ years of experience building scalable web applications with React, Solid.js, Node.js, and modern frontend tooling.