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.
Comments