Skip to main content

SolidJS Complete Guide: Fine-Grained Reactivity in 2026

Created: March 7, 2026 CalmOps 22 min read

Introduction

In the ever-evolving landscape of JavaScript frameworks, SolidJS has emerged as a standout performer, challenging the dominance of React, Vue, and Svelte with its unique approach to reactivity. Unlike traditional frameworks that use a virtual DOM, SolidJS employs fine-grained reactivity with direct DOM updates, resulting in exceptional performance characteristics that have captured the attention of developers building performance-critical applications.

The framework’s architecture represents a fundamental shift in how we think about UI state management. Instead of re-rendering component trees and diffing the results, SolidJS tracks dependencies at the signal level and updates only the exact DOM nodes that change. This approach eliminates the overhead of reconciliation while maintaining developer ergonomics that feel familiar to React developers.

As we move through 2026, SolidJS has matured significantly with improved tooling, a growing ecosystem, and increased enterprise adoption. This comprehensive guide explores everything from fundamental concepts to advanced patterns, helping you understand why SolidJS might be the right choice for your next project.

What is SolidJS?

SolidJS is a declarative JavaScript library for building user interfaces that uses fine-grained reactivity as its core architectural principle. Created by Ryan Carniato in 2018, the framework has gained substantial traction due to its unique performance characteristics and innovative approach to state management.

Core Philosophy

The fundamental difference between SolidJS and other frameworks lies in its execution model. When you define a component in SolidJS, it runs only once during initialization. This is radically different from React, where components re-execute on every state change. In SolidJS, the return value is a static tree of DOM nodes with reactive signals embedded within them.

This architectural choice means that your component functions are essentially factories that create the UI structure once. The reactive system then takes over, directly manipulating the DOM based on signal changes without any virtual DOM overhead. The implications for performance are profound: you’re essentially getting the raw speed of imperative DOM manipulation with the declarative syntax of a modern framework.

The Signal Primitive

At the heart of SolidJS lies the signal, a primitive that represents a piece of reactive state. Signals are more than just observable values; they form the foundation of the entire reactivity system through a mechanism called dependency tracking.

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

function Counter() {
  const [count, setCount] = createSignal(0);
  
  createEffect(() => {
    console.log(`The count is now: ${count()}`);
  });
  
  return (
    <button onClick={() => setCount(c => c + 1)}>
      Count: {count()}
    </button>
  );
}

When you call a signal getter like count(), SolidJS automatically tracks that access as a dependency. When the signal’s value changes, only the code that specifically reads that signal gets re-executed. This granular approach means you never pay for updates you don’t need.

The signal system in SolidJS draws inspiration fromcell-based spreadsheets and observable libraries, but implements them with compile-time optimizations that make them practical for real-world applications. The framework uses JavaScript Proxies to create reactive objects that can track property access, enabling reactivity on plain objects without requiring special wrapper classes.

Why Fine-Grained Reactivity Matters

Traditional virtual DOM frameworks work by re-rendering entire component subtrees when state changes, then calculating the differences between the old and new trees. While clever optimizations have made this approach viable, it still involves significant overhead. Every render creates new JavaScript objects, traverses the component tree, and performs diffing calculations—even when most of the DOM hasn’t changed.

Fine-grained reactivity eliminates this overhead by understanding exactly which pieces of state affect which parts of the UI. When a signal updates, SolidJS knows precisely which effects depend on that signal and runs only those effects. The DOM updates happen directly, without any intermediate representation or diffing process.

Consider a list component with thousands of items. In a virtual DOM framework, filtering that list requires re-rendering the entire list component, creating thousands of virtual DOM nodes, and reconciling them with the previous version. In SolidJS, only the specific DOM nodes that need to appear or disappear get updated. The difference in performance can be orders of magnitude for large lists.

Setting Up SolidJS

Getting started with SolidJS is straightforward, with multiple options depending on your project requirements and existing workflow. The framework provides official templates for various build tools and supports both TypeScript and JavaScript out of the box.

Using the CLI

The fastest way to create a new SolidJS project is through the official CLI, which scaffolds a project with sensible defaults:

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

For TypeScript projects, use the TypeScript template:

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

The CLI creates a minimal but complete project structure with Vite as the build tool, Hot Module Replacement enabled, and proper development server configuration. The template includes essential dependencies and demonstrates basic patterns for components, routing, and state management.

Integration with Existing Projects

SolidJS can be integrated into existing applications through its reactive primitives, allowing you to adopt it incrementally. The framework’s small size—approximately 7KB gzipped—makes it practical to include in legacy applications for specific interactive features:

<script type="module">
  import { createSignal, render } from 'solid-js/web';
  import { createApp } from 'solid-js';
  
  function Counter() {
    const [count, setCount] = createSignal(0);
    
    return () => (
      <div class="counter">
        <button onClick={() => setCount(c => c - 1)}>-</button>
        <span>{count()}</span>
        <button onClick={() => setCount(c => c + 1)}>+</button>
      </div>
    );
  }
  
  render(Counter, document.getElementById('app'));
</script>

This flexibility allows teams to experiment with SolidJS in production environments without committing to a full rewrite. You can render SolidJS components into specific DOM nodes within larger applications, mixing and matching with other frameworks or vanilla JavaScript.

SolidJS Starter Templates

Beyond the basic templates, the community has created specialized starters for various use cases:

The SolidStart meta-framework provides server-side rendering, file-based routing, and data fetching capabilities similar to Next.js or Remix. It’s ideal for building full-stack applications with SolidJS:

npm create solid@latest
# Select "SolidStart" when prompted

For mobile applications, SolidJS can be used with Capacitor to create cross-platform apps that feel native. The Solid Native project provides React-like primitives for building mobile interfaces with SolidJS’s performance characteristics.

The Solid App Router starter includes pre-configured routing, lazy loading, and authentication patterns suitable for building Single Page Applications with complex navigation requirements.

Core Concepts and Primitives

Understanding SolidJS’s reactivity system requires grasping its core primitives: signals, effects, memos, and resources. These building blocks combine to create a powerful yet predictable state management system.

Signals and State

Signals are the foundation of reactivity in SolidJS. They hold values that can change over time, and any code that reads a signal automatically subscribes to changes:

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

function createCounter() {
  const [count, setCount] = createSignal(0);
  const [double, setDouble] = createSignal(0);
  
  // This effect runs whenever count changes
  createEffect(() => {
    setDouble(count() * 2);
  });
  
  return { count, double, increment: () => setCount(c => c + 1) };
}

// Signals must be created within a reactive context
const counter = createRoot(() => createCounter());

Beyond basic signals, SolidJS provides derived state through computed values. The createMemo function creates memoized computations that only recalculate when their dependencies change:

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

function TodoList({ todos }) {
  const completedCount = createMemo(() => 
    todos().filter(todo => todo.completed).length
  );
  
  const percentage = createMemo(() => {
    const total = todos().length;
    if (total === 0) return 0;
    return Math.round((completedCount() / total) * 100);
  });
  
  return (
    <div>
      <p>Progress: {percentage()}%</p>
      <p>Completed: {completedCount()} of {todos().length}</p>
    </div>
  );
}

Memos are particularly valuable for expensive calculations that should only run when necessary. The memoized function caches its result and only recomputes when input signals change, preventing unnecessary recalculation on every render.

Effects and Side Effects

Effects handle side effects in SolidJS, running code in response to signal changes. They bridge the gap between the reactive world and the outside world, handling operations like DOM manipulation, network requests, and logging:

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

function SearchComponent() {
  const [query, setQuery] = createSignal('');
  const [results, setResults] = createSignal([]);
  
  // Debounced search effect
  createEffect(() => {
    const searchTerm = query();
    const timeoutId = setTimeout(async () => {
      if (searchTerm) {
        const response = await fetch(`/api/search?q=${searchTerm}`);
        const data = await response.json();
        setResults(data);
      }
    }, 300);
    
    // Cleanup function runs before effect re-runs or when component unmounts
    onCleanup(() => clearTimeout(timeoutId));
  });
  
  return (
    <div>
      <input 
        type="text" 
        value={query()} 
        onInput={(e) => setQuery(e.target.value)}
      />
      <ul>
        {results().map(item => <li>{item.name}</li>)}
      </ul>
    </div>
  );
}

Effects run after the DOM has been updated, making them safe for operations that need to read the current DOM state. The cleanup mechanism ensures that resources are properly released and prevents memory leaks from stale effects.

Resources for Async Data

SolidJS provides the createResource primitive specifically for handling async data fetching. Resources integrate loading states, error handling, and caching into a cohesive API:

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

async function fetchUser(id) {
  const response = await fetch(`/api/users/${id}`);
  return response.json();
}

function UserProfile({ userId }) {
  const [user, { refetch, mutate }] = createResource(userId, fetchUser);
  
  return (
    <div>
      <Show when={!user.loading} fallback={<p>Loading...</p>}>
        <Show when={!user.error} fallback={<p>Error: {user.error}</p>}>
          <h1>{user().name}</h1>
          <p>{user().email}</p>
          <button onClick={() => refetch()}>Refresh</button>
        </Show>
      </Show>
    </div>
  );
}

The resource primitive handles the async lifecycle automatically, providing loading, ready, and error properties that make building loading states straightforward. The mutate function allows optimistic updates by directly modifying the cached value before the async operation completes.

For more complex async patterns, SolidJS supports streaming SSR and Suspense, enabling progressive loading of page content with graceful degradation for slower network connections.

Stores for Complex State

While signals work well for simple values, the createStore function provides a mutable API for complex nested state. Stores use proxies to track property access deeply, enabling fine-grained reactivity for objects and arrays:

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

function TodoApp() {
  const [state, setState] = createStore({
    todos: [],
    filter: 'all',
    newTodo: ''
  });
  
  const addTodo = () => {
    if (!state.newTodo.trim()) return;
    
    setState(produce(s => {
      s.todos.push({
        id: Date.now(),
        text: s.newTodo,
        completed: false
      });
      s.newTodo = '';
    }));
  };
  
  const toggleTodo = (id) => {
    setState(
      'todos',
      todo => todo.id === id,
      'completed',
      completed => !completed
    );
  };
  
  return (
    <div>
      <input 
        value={state.newTodo}
        onInput={(e) => setState('newTodo', e.target.value)}
        onKeyPress={(e) => e.key === 'Enter' && addTodo()}
      />
      <button onClick={addTodo}>Add</button>
      <ul>
        {state.todos.map(todo => (
          <li>
            <input 
              type="checkbox" 
              checked={todo.completed}
              onChange={() => toggleTodo(todo.id)}
            />
            {todo.text}
          </li>
        ))}
      </ul>
    </div>
  );
}

The store API supports path-based updates and conditional modifications, making it comfortable for developers coming from Redux or similar state management libraries. The produce utility provides a Immer-like experience for immutable updates.

Component Patterns

SolidJS encourages specific patterns for organizing code that leverage its reactivity system effectively. Understanding these patterns helps you write idiomatic code that performs well and scales maintainably.

Control Flow Components

SolidJS provides built-in components for handling conditional rendering and lists efficiently. Unlike React’s map and conditional operators, these components are optimized for the fine-grained reactivity system:

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

function UserDashboard({ user }) {
  return (
    <Switch>
      <Match when={user.loading}>
        <div class="loading-spinner">Loading...</div>
      </Match>
      <Match when={user.error}>
        <div class="error-message">
          Error loading user: {user.error.message}
        </div>
      </Match>
      <Match when={user()}>
        <div class="dashboard">
          <h1>Welcome, {user().name}!</h1>
          <Show when={user().isAdmin}>
            <AdminPanel />
          </Show>
          <ul class="recent-items">
            <For each={user().recentItems}>
              {(item) => <li>{item.name}</li>}
            </For>
          </ul>
        </div>
      </Match>
    </Switch>
  );
}

The For component is particularly powerful—it creates a keyed list where each item maintains its DOM node identity. When the list order changes, SolidJS moves DOM nodes rather than recreating them, maintaining focus and scroll position better than diff-based approaches.

The Show and Switch components handle conditional rendering with automatic cleanup. When conditions change, SolidJS efficiently adds or removes DOM nodes without unnecessary re-renders of sibling components.

Composing Components

Component composition in SolidJS follows a functional style, with props flowing downward and callbacks flowing upward. The framework’s reactivity system makes composition patterns particularly elegant:

import { createSignal, createContext, useContext } from 'solid-js';

const ThemeContext = createContext();

function ThemeProvider({ children }) {
  const [theme, setTheme] = createSignal('light');
  
  const toggleTheme = () => setTheme(t => t === 'light' ? 'dark' : 'light');
  
  const value = {
    theme,
    toggleTheme,
    isDark: () => theme() === 'dark'
  };
  
  return (
    <ThemeContext.Provider value={value}>
      {children}
    </ThemeContext.Provider>
  );
}

function ThemedButton(props) {
  const theme = useContext(ThemeContext);
  
  return (
    <button 
      class={`btn btn-${theme.theme()}`}
      onClick={props.onClick}
    >
      {props.children}
    </button>
  );
}

Context works seamlessly with signals, automatically propagating changes throughout the component tree. When the context value changes, only components that actually read specific properties re-render—everything else stays static.

Refs and Direct DOM Access

Sometimes you need direct access to DOM nodes for focus management, measurements, or integrating with non-reactive libraries. SolidJS provides refs for this purpose:

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

function AutoFocusInput() {
  let inputRef;
  
  onMount(() => {
    inputRef.focus();
  });
  
  return (
    <input 
      ref={inputRef} 
      type="text" 
      placeholder="Auto-focused on mount"
    />
  );
}

Refs in SolidJS are straightforward—you assign a variable to the ref attribute, and the DOM node becomes available after mounting. This pattern integrates well with libraries that require direct DOM access, such as mapping libraries or rich text editors.

State Management Comparison

Understanding how SolidJS’s state management compares to other approaches helps you make informed architectural decisions. The framework offers multiple options depending on your complexity requirements.

Local Component State

For simple components, local signals provide everything you need:

function LoginForm() {
  const [email, setEmail] = createSignal('');
  const [password, setPassword] = createSignal('');
  const [error, setError] = createSignal(null);
  const [loading, setLoading] = createSignal(false);
  
  const handleSubmit = async (e) => {
    e.preventDefault();
    setLoading(true);
    setError(null);
    
    try {
      await authenticate(email(), password());
    } catch (err) {
      setError(err.message);
    } finally {
      setLoading(false);
    }
  };
  
  return (
    <form onSubmit={handleSubmit}>
      <Show when={error()}>
        <div class="error">{error()}</div>
      </Show>
      <input 
        type="email" 
        value={email()} 
        onInput={(e) => setEmail(e.target.value)} 
      />
      <input 
        type="password" 
        value={password()} 
        onInput={(e) => setPassword(e.target.value)} 
      />
      <button type="submit" disabled={loading()}>
        {loading() ? 'Signing in...' : 'Sign In'}
      </button>
    </form>
  );
}

This pattern works beautifully for form handling and local UI state. The signal-based approach eliminates the need for separate state management libraries in most cases.

Global State Patterns

For application-wide state, SolidJS supports several patterns. The simplest is creating signals in a module:

// store/user.js
import { createSignal } from 'solid-js';

const [user, setUser] = createSignal(null);
const [isAuthenticated, setIsAuthenticated] = createSignal(false);

export { user, setUser, isAuthenticated, setIsAuthenticated };

This module-level pattern works well for simple global state but has limitations for more complex scenarios. For larger applications, the Context API combined with stores provides better organization:

// store/app.js
import { createContext, useContext } from 'solid-js';
import { createStore } from 'solid-js/store';

const AppContext = createContext();

export function AppProvider(props) {
  const [state, setState] = createStore({
    user: null,
    preferences: {
      theme: 'light',
      language: 'en'
    },
    notifications: []
  });
  
  const actions = {
    login: (userData) => setState('user', userData),
    logout: () => setState('user', null),
    setTheme: (theme) => setState('preferences', 'theme', theme),
    addNotification: (notification) => {
      setState('notifications', n => [...n, { ...notification, id: Date.now() }]);
    }
  };
  
  return (
    <AppContext.Provider value={{ state, ...actions }}>
      {props.children}
    </AppContext.Provider>
  );
}

export function useApp() {
  return useContext(AppContext);
}

This pattern provides a Redux-like experience with the performance benefits of SolidJS’s fine-grained reactivity. Actions can modify state directly while components that read specific properties only re-render when those properties change.

Comparison with Other Solutions

Compared to Redux, SolidJS state management requires far less boilerplate. There’s no need for actions, reducers, selectors, or provider components—just signals and stores:

// Redux approach (verbose)
const INCREMENT = 'INCREMENT';
const increment = () => ({ type: INCREMENT });
const counterReducer = (state = 0, action) => {
  switch (action.type) {
    case INCREMENT: return state + 1;
    default: return state;
  }
};
const store = createStore(counterReducer);
const incrementAction = increment();
store.dispatch(incrementAction);

// SolidJS approach (minimal)
const [count, setCount] = createSignal(0);
setCount(c => c + 1);

The trade-off is that Redux provides time-travel debugging and predictable state histories, which can be valuable in large teams or complex undo/redo scenarios. For most applications, SolidJS’s simpler model offers equivalent functionality with less code.

Performance Optimization

SolidJS’s fine-grained reactivity provides excellent performance out of the box, but understanding optimization techniques helps you build truly high-performance applications.

Understanding Reactivity Overhead

Even though SolidJS is highly optimized, there are patterns that can create unnecessary overhead. The key is understanding what triggers reactivity:

// This creates a new function on every render (expensive in loops)
<For each={items()}>
  {(item) => (
    <button onClick={() => handleClick(item.id)}>
      {item.name}
    </button>
  )}
</For>

// Better: pre-bind the handler
const handleClick = (id) => {
  // handler logic
};

<For each={items()}>
  {(item) => (
    <button onClick={handleClick.bind(null, item.id)}>
      {item.name}
    </button>
  )}
</For>

// Even better: use data attributes for delegation
<For each={items()}>
  {(item) => (
    <button data-id={item.id} class="item-button">
      {item.name}
    </button>
  )}
</For>

Event delegation—attaching a single handler at a parent level rather than individual handlers on each child—can significantly reduce memory usage and improve performance for large lists.

Memoization Strategies

While SolidJS automatically tracks dependencies, explicit memoization can help for expensive computations:

import { createMemo, createMemo as useMemo } from 'solid-js';

function ExpensiveComponent({ data }) {
  // These recalculate only when data changes
  const sortedData = createMemo(() => {
    return [...data()].sort((a, b) => b.value - a.value);
  });
  
  const filteredData = createMemo(() => {
    return sortedData().filter(item => item.active);
  });
  
  const statistics = createMemo(() => {
    const values = filteredData();
    return {
      count: values.length,
      average: values.reduce((sum, v) => sum + v.value, 0) / values.length,
      total: values.reduce((sum, v) => sum + v.value, 0)
    };
  });
  
  return <SummaryStats stats={statistics()} />;
}

The memoization chain—where each memo depends on the previous one—ensures that expensive operations run only when necessary. When data changes, the entire chain recalculates. When only filteredData is read, only that memo recalculates.

Code Splitting and Lazy Loading

SolidJS supports dynamic imports for code splitting, reducing initial bundle size:

import { lazy, Suspense } from 'solid-js';

const HeavyChart = lazy(() => import('./HeavyChart'));

function Dashboard() {
  return (
    <div class="dashboard">
      <SummaryCards />
      <Suspense fallback={<div class="chart-skeleton">Loading chart...</div>}>
        <HeavyChart data={props.data} />
      </Suspense>
    </div>
  );
}

The lazy function creates a component that only loads its code when rendered. Combined with Suspense, this provides a smooth experience with placeholder content while the chunk loads.

Virtual Lists for Large Data

For very large lists, virtual scrolling is essential. SolidJS has several virtual list implementations available:

import { createVirtualizer } from '@tanstack/solid-virtual';

function VirtualList({ items }) {
  const virtualizer = createVirtualizer({
    count: items().length,
    getScrollElement: () => document.body,
    estimateSize: () => 50,
    renderSlice: (index, count) => (
      <For each={items().slice(index, index + count)}>
        {(item) => <div class="list-item">{item.name}</div>}
      </For>
    )
  });
  
  return (
    <div style={{ height: '500px', overflow: 'auto' }}>
      <div style={{ height: `${virtualizer.getTotalSize()}px` }}>
        <For each={virtualizer.getVirtualItems()}>
          {(virtualRow) => (
            <div
              style={{
                position: 'absolute',
                top: 0,
                transform: `translateY(${virtualRow.start}px)`,
                height: `${virtualRow.size}px`
              }}
            >
              {items()[virtualRow.index].name}
            </div>
          )}
        </For>
      </div>
    </div>
  );
}

Virtual scrolling renders only the visible items plus a small buffer, allowing lists with millions of items to scroll smoothly. This technique is crucial for data-intensive applications like dashboards, tables, and feeds.

Ecosystem and Libraries

The SolidJS ecosystem has grown substantially, with solutions for routing, styling, data fetching, and testing that leverage the framework’s unique characteristics.

Routing

Solid App Router provides comprehensive routing capabilities:

import { Router, Route, A } from '@solidjs/router';

function App() {
  return (
    <Router>
      <nav>
        <A href="/">Home</A>
        <A href="/about">About</A>
        <A href="/users">Users</A>
      </nav>
      <Route path="/" component={Home} />
      <Route path="/about" component={About} />
      <Route path="/users" component={Users} />
      <Route path="/users/:id" component={UserProfile} />
    </Router>
  );
}

function UserProfile(props) {
  return <p>User ID: {props.params.id}</p>;
}

The router supports nested routes, route guards, animated transitions, and lazy loading. Its design emphasizes performance and simplicity while providing all the features expected from a modern SPA router.

Styling Solutions

SolidJS works with various styling approaches. For scoped styles, the framework supports CSS modules natively:

import styles from './Button.module.css';

function Button(props) {
  return (
    <button class={styles.button}>
      {props.children}
    </button>
  );
}

For component-scoped CSS-in-JS, libraries like solid-styled-components provide a familiar API:

import { styled } from 'solid-styled-components';

const Button = styled.button`
  background: blue;
  color: white;
  padding: 8px 16px;
  border-radius: 4px;
  
  &:hover {
    background: darkblue;
  }
`;

Tailwind CSS integrates well with SolidJS through PostCSS configuration, providing utility-first styling without runtime overhead:

function Card({ title, children }) {
  return (
    <div class="bg-white rounded-lg shadow-md p-6">
      <h2 class="text-xl font-bold mb-4">{title}</h2>
      <div class="text-gray-600">{children}</div>
    </div>
  );
}

Data Fetching

TanStack Query (formerly React Query) has been ported to SolidJS, providing sophisticated caching and synchronization:

import { createQuery } from '@tanstack/solid-query';

function UserList() {
  const query = createQuery(() => ({
    queryKey: ['users'],
    queryFn: async () => {
      const response = await fetch('/api/users');
      return response.json();
    },
    staleTime: 5000
  }));
  
  return (
    <div>
      <Show when={query.isLoading}>
        <p>Loading...</p>
      </Show>
      <Show when={query.isError}>
        <p>Error: {query.error.message}</p>
      </Show>
      <Show when={query.isSuccess}>
        <ul>
          <For each={query.data}>
            {(user) => <li>{user.name}</li>}
          </For>
        </ul>
      </Show>
    </div>
  );
}

The library handles caching, background refetching, deduplication, and optimistic updates with a minimal API. Combined with Solid’s reactivity, it provides an excellent developer experience.

Testing

SolidJS Testing Library follows the same philosophy as React Testing Library—testing behavior rather than implementation:

import { render, screen, fireEvent } from '@solidjs/testing-library';
import { describe, it, expect, vi } from 'vitest';
import Counter from './Counter';

describe('Counter', () => {
  it('renders initial count', () => {
    render(() => <Counter />);
    expect(screen.getByText('Count: 0')).toBeInTheDocument();
  });
  
  it('increments count on click', () => {
    render(() => <Counter />);
    const button = screen.getByRole('button', { name: /increment/i });
    fireEvent.click(button);
    expect(screen.getByText('Count: 1')).toBeInTheDocument();
  });
});

Vitest provides the test runner with fast execution and native ESM support. The testing library’s query-based assertions make tests resilient to implementation changes.

Server-Side Rendering with SolidStart

SolidStart extends SolidJS with full-stack capabilities, providing server-side rendering, file-based routing, and API routes similar to Next.js or Remix.

Getting Started with SolidStart

Create a new SolidStart project:

npm create solid@latest
# Select "SolidStart" with TypeScript

The project structure follows familiar conventions:

src/
├── routes/
│   ├── index.tsx
│   ├── about.tsx
│   └── users/
│       ├── index.tsx
│       └── [id].tsx
├── components/
├── lib/
└── app.tsx

Routes use file-based routing, with dynamic segments created through bracket notation. The framework handles both client and server rendering seamlessly.

Data Fetching in SolidStart

SolidStart provides createServerData and createServerAction for server-side data operations:

import { createServerData$ } from 'solid-start/server';
import { For, Show } from 'solid-js';

const fetchUsers = createServerData$(async () => {
  const db = await getDatabase();
  return db.users.findMany();
});

function UsersPage() {
  const users = fetchUsers();
  
  return (
    <div>
      <h1>Users</h1>
      <Show when={users()} fallback={<p>Loading...</p>}>
        <ul>
          <For each={users()}>
            {(user) => (
              <li>
                <a href={`/users/${user.id}`}>{user.name}</a>
              </li>
            )}
          </For>
        </ul>
      </Show>
    </div>
  );
}

export default UsersPage;

The $ suffix indicates server-only functions—they run exclusively on the server and their results are serialized to the client. This approach keeps sensitive logic and database queries secure while providing a smooth developer experience.

Form Handling and Actions

SolidStart’s form handling uses progressive enhancement, working even without JavaScript:

import { createServerAction$, redirect } from 'solid-start/server';

const addUser = createServerAction$(async (formData) => {
  const db = await getDatabase();
  const name = formData.get('name');
  
  await db.users.create({ name });
  
  return redirect('/users');
});

function AddUserForm() {
  const adding = addUser.use();
  
  return (
    <form action={addUser} method="post">
      <label>
        Name:
        <input type="text" name="name" required />
      </label>
      <button type="submit" disabled={adding.pending}>
        {adding.pending ? 'Adding...' : 'Add User'}
      </button>
    </form>
  );
}

Actions handle form submissions on the server, with built-in pending states for optimistic UI. The approach maintains functionality even when JavaScript fails or hasn’t loaded yet.

Real-World Examples

Understanding how SolidJS performs in production helps validate its suitability for your projects. Several notable companies have adopted SolidJS for performance-critical applications.

E-commerce Applications

One major e-commerce platform rebuilt their product listing pages using SolidJS, achieving a 40% reduction in JavaScript bundle size and a 65% improvement in Time to Interactive. The fine-grained reactivity proved particularly valuable for dynamic filters and real-time inventory updates.

The combination of signals for local state and stores for global cart state provided excellent developer ergonomics while maintaining performance. Code splitting for product images and the ability to lazy load entire feature sections contributed to fast initial loads.

Data Visualization Dashboards

A fintech company migrated their trading dashboards from React to SolidJS, immediately seeing improvements in real-time data update performance. Traditional virtual DOM diffing became a bottleneck when processing hundreds of price updates per second. SolidJS’s direct DOM updates handled the same volume with minimal overhead.

The integration with WebGL-based charting libraries was seamless—refs provided direct canvas access while signals managed the reactive data flow. Custom hooks abstracted the complexity of managing WebGL contexts within the reactive system.

Progressive Web Apps

A news publisher built their PWA using SolidJS, leveraging the framework’s small footprint to achieve excellent performance on low-end mobile devices. The service worker integration worked naturally with SolidStart’s SSR, enabling reliable offline reading and fast subsequent visits.

The island architecture pattern—using partial hydration for interactive components while rendering static content server-side—provided an ideal balance of interactivity and performance for content-heavy applications.

Best Practices

After building several production applications with SolidJS, certain patterns have emerged as particularly effective for maintainable codebases.

Project Structure

Organize code by feature rather than by type:

src/
├── features/
│   ├── auth/
│   │   ├── components/
│   │   ├── hooks/
│   │   ├── api.ts
│   │   └── index.ts
│   ├── products/
│   │   ├── components/
│   │   ├── hooks/
│   │   └── types.ts
│   └── cart/
├── shared/
│   ├── components/
│   ├── hooks/
│   └── utils/
└── app.tsx

This structure colocates related code, making it easier to understand and modify features. Shared utilities live in a separate directory, but feature-specific logic stays with its feature.

TypeScript Usage

TypeScript integration in SolidJS is excellent. Define prop types explicitly:

import { Component, JSX } from 'solid-js';

interface ButtonProps {
  variant?: 'primary' | 'secondary' | 'danger';
  size?: 'sm' | 'md' | 'lg';
  disabled?: boolean;
  onClick?: (e: MouseEvent) => void;
  children: JSX.Element;
}

const Button: Component<ButtonProps> = (props) => {
  return (
    <button
      class={`btn btn-${props.variant ?? 'primary'} btn-${props.size ?? 'md'}`}
      disabled={props.disabled}
      onClick={props.onClick}
    >
      {props.children}
    </button>
  );
};

export default Button;

The type inference works well with signals and stores, reducing the need for explicit type annotations while maintaining type safety.

Error Boundaries

SolidJS provides error boundaries to gracefully handle component errors:

import { ErrorBoundary, FallbackProps } from 'solid-js';

function ErrorFallback(props: FallbackProps) {
  return (
    <div class="error-boundary">
      <h2>Something went wrong</h2>
      <p>{props.error.message}</p>
      <button onClick={props.reset}>Try again</button>
    </div>
  );
}

function App() {
  return (
    <ErrorBoundary fallback={ErrorFallback}>
      <MainContent />
    </ErrorBoundary>
  );
}

Error boundaries catch JavaScript errors in their subtree, displaying fallback UI while preventing the entire application from crashing. They can be nested for granular error handling.

Conclusion

SolidJS represents a significant evolution in how we think about building reactive user interfaces. Its fine-grained reactivity system delivers exceptional performance without sacrificing developer experience, offering a compelling alternative to virtual DOM-based frameworks.

The framework’s strengths—small bundle size, direct DOM updates, and minimal runtime overhead—make it particularly well-suited for performance-critical applications, mobile-first experiences, and scenarios where every millisecond matters. The growing ecosystem, with SolidStart providing full-stack capabilities, addresses most application needs without requiring additional frameworks.

As the JavaScript ecosystem continues to mature, SolidJS’s architectural choices position it well for long-term success. The signals-based reactivity model has been adopted by other frameworks, suggesting it’s becoming a standard pattern for reactive UI development.

Whether you’re building a new application from scratch or considering migration from an existing framework, SolidJS deserves serious consideration. Its combination of performance, developer experience, and architectural soundness makes it a framework worth mastering in 2026 and beyond.

Resources

Comments

Share this article

Scan to read on mobile