SolidJS
Fine-grained reactive JavaScript UI framework with no virtual DOM and exceptional performance.
Installation
Section titled “Installation”Create New Project
Section titled “Create New Project”| Command | Description |
|---|---|
npx degit solidjs/templates/ts my-app | Create TypeScript SolidJS project |
npx degit solidjs/templates/js my-app | Create JavaScript SolidJS project |
npm create solid@latest | Create project with SolidStart CLI |
npm install solid-js | Install SolidJS in existing project |
npm install solid-start | Install SolidStart meta-framework |
Development Commands
Section titled “Development Commands”| Command | Description |
|---|---|
npm run dev | Start development server with hot reload |
npm run build | Build for production |
npm run serve | Preview production build locally |
npm run start | Start SolidStart server |
Project Templates
Section titled “Project Templates”# Bare SolidJS (Vite)
npx degit solidjs/templates/ts my-app
cd my-app && npm install
# SolidStart (full-stack meta-framework)
npm create solid@latest my-app
# Choose template: bare, with-auth, with-mdx, with-prisma, with-tailwindcss
# Community templates
npx degit solidjs/templates/ts-windicss my-app # WindiCSS
npx degit solidjs/templates/ts-unocss my-app # UnoCSS
npx degit solidjs/templates/ts-bootstrap my-app # Bootstrap
npx degit solidjs/templates/ts-sass my-app # Sass
Components & JSX
Section titled “Components & JSX”Basic Components
Section titled “Basic Components”| Command | Description |
|---|---|
function App() { return <div>Hello</div> } | Define a basic component |
<MyComponent name="value" /> | Pass props to a component |
props.children | Access children content |
<div class={styles.container}> | Apply CSS class (use class, not className) |
<div classList={{ active: isActive() }}> | Conditional CSS classes |
<div style={{ color: 'red', 'font-size': '14px' }}> | Apply inline styles |
<div innerHTML={htmlString} /> | Set raw HTML content |
<div ref={myRef}> | Attach DOM reference |
Event Handling
Section titled “Event Handling”| Command | Description |
|---|---|
<div onClick={handler}> | Attach delegated event |
<div on:click={handler}> | Attach native DOM event (bypasses delegation) |
<div onInput={(e) => setValue(e.target.value)}> | Handle input events |
<div on:keydown={(e) => handleKey(e)}> | Handle keyboard events natively |
<div onClick={[handler, data]}> | Pass data with event handler |
Component Patterns
Section titled “Component Patterns”import { type Component, type ParentComponent } from 'solid-js'
// Typed component with props
interface UserProps {
name: string
age: number
role?: string
}
const UserCard: Component<UserProps> = (props) => {
return (
<div class="card">
<h2>{props.name}</h2>
<p>Age: {props.age}</p>
<p>Role: {props.role ?? 'Member'}</p>
</div>
)
}
// Parent component (accepts children)
const Layout: ParentComponent = (props) => {
return (
<div class="layout">
<header>My App</header>
<main>{props.children}</main>
<footer>Footer</footer>
</div>
)
}
// Splitting props for forwarding
import { splitProps } from 'solid-js'
const Button: Component<ButtonProps> = (props) => {
const [local, rest] = splitProps(props, ['variant', 'size'])
return (
<button
class={`btn btn-${local.variant} btn-${local.size}`}
{...rest}
/>
)
}
// Merging default props
import { mergeProps } from 'solid-js'
const Alert: Component<AlertProps> = (rawProps) => {
const props = mergeProps({ type: 'info', dismissible: false }, rawProps)
return <div class={`alert alert-${props.type}`}>{props.children}</div>
}
Reactive Primitives
Section titled “Reactive Primitives”Signals
Section titled “Signals”| Command | Description |
|---|---|
const [count, setCount] = createSignal(0) | Create a reactive signal |
count() | Read signal value (must call as function) |
setCount(5) | Set signal to specific value |
setCount(prev => prev + 1) | Update signal with previous value |
const [name, setName] = createSignal<string>() | Create typed signal |
Effects & Memos
Section titled “Effects & Memos”| Command | Description |
|---|---|
createEffect(() => console.log(count())) | Run side effect on signal change |
createEffect(on(count, (v) => log(v))) | Explicitly track specific signal |
createEffect(on([a, b], ([a, b]) => ...)) | Track multiple signals explicitly |
const double = createMemo(() => count() * 2) | Create derived/computed value |
createRenderEffect(() => ...) | Effect that runs before DOM paint |
createComputed(() => ...) | Synchronous effect during rendering |
Lifecycle
Section titled “Lifecycle”| Command | Description |
|---|---|
onMount(() => { ... }) | Run once when component mounts |
onCleanup(() => { ... }) | Run cleanup when component unmounts |
batch(() => { setA(1); setB(2) }) | Batch multiple signal updates |
untrack(() => value()) | Read signal without tracking dependency |
Reactive Patterns
Section titled “Reactive Patterns”import { createSignal, createEffect, createMemo, on, batch, untrack } from 'solid-js'
function Counter() {
const [count, setCount] = createSignal(0)
const [step, setStep] = createSignal(1)
// Derived value (memoized, recalculates only when count changes)
const doubled = createMemo(() => count() * 2)
const isEven = createMemo(() => count() % 2 === 0)
// Effect: runs when any accessed signal changes
createEffect(() => {
console.log(`Count is now: ${count()}`)
// Automatically tracks count() as a dependency
})
// Explicit tracking with on()
createEffect(on(count, (value, prev) => {
console.log(`Changed from ${prev} to ${value}`)
}, { defer: true })) // defer: skip initial run
// Batch updates (single re-render)
const reset = () => batch(() => {
setCount(0)
setStep(1)
})
// Read without creating dependency
const logWithoutTracking = () => {
const current = untrack(() => count())
console.log('Snapshot:', current)
}
return (
<div>
<p>Count: {count()} (doubled: {doubled()}, even: {String(isEven())})</p>
<button onClick={() => setCount(c => c + step())}>
Add {step()}
</button>
<button onClick={reset}>Reset</button>
</div>
)
}
Control Flow
Section titled “Control Flow”Built-in Components
Section titled “Built-in Components”| Command | Description |
|---|---|
<Show when={loggedIn()} fallback={<Login/>}> | Conditional rendering |
<For each={items()}>{(item) => <li>{item}</li>}</For> | Render list (keyed by reference) |
<Index each={items()}>{(item, i) => <li>{item()}</li>}</Index> | Render list (keyed by index) |
<Switch><Match when={a()}>A</Match></Switch> | Switch/case rendering |
<ErrorBoundary fallback={err => <p>{err}</p>}> | Catch render errors |
<Suspense fallback={<Loading/>}> | Show fallback during async loading |
<Dynamic component={MyComp} /> | Render dynamic component |
<Portal mount={document.body}> | Render into different DOM node |
Control Flow Examples
Section titled “Control Flow Examples”import { Show, For, Index, Switch, Match, Suspense, ErrorBoundary } from 'solid-js'
function Dashboard() {
const [user, setUser] = createSignal(null)
const [items, setItems] = createSignal([
{ id: 1, name: 'Alpha', status: 'active' },
{ id: 2, name: 'Beta', status: 'inactive' },
])
const [view, setView] = createSignal('list')
return (
<div>
{/* Show: conditional rendering with fallback */}
<Show when={user()} fallback={<p>Please log in</p>}>
{(u) => <p>Welcome, {u().name}!</p>}
</Show>
{/* For: keyed by reference (best for objects) */}
<For each={items()}>
{(item, index) => (
<div>
<span>{index() + 1}. {item.name}</span>
<span> ({item.status})</span>
</div>
)}
</For>
{/* Switch/Match: multiple conditions */}
<Switch fallback={<p>Unknown view</p>}>
<Match when={view() === 'list'}>
<ListView />
</Match>
<Match when={view() === 'grid'}>
<GridView />
</Match>
<Match when={view() === 'table'}>
<TableView />
</Match>
</Switch>
{/* ErrorBoundary: catch errors in child tree */}
<ErrorBoundary fallback={(err, reset) => (
<div>
<p>Error: {err.message}</p>
<button onClick={reset}>Try Again</button>
</div>
)}>
<RiskyComponent />
</ErrorBoundary>
</div>
)
}
Stores & State
Section titled “Stores & State”Store Operations
Section titled “Store Operations”| Command | Description |
|---|---|
const [store, setStore] = createStore({}) | Create a reactive store |
setStore('name', 'Alice') | Update store property by path |
setStore('users', 0, 'name', 'Bob') | Update nested store property |
setStore('list', l => [...l, item]) | Push item to store array |
setStore('list', i => i.id === 1, 'done', true) | Update matching array items |
produce(s => { s.count++ }) | Mutable-style store updates |
reconcile(newData) | Replace store data efficiently |
unwrap(store) | Get raw non-proxied data |
Store Patterns
Section titled “Store Patterns”import { createStore, produce, reconcile, unwrap } from 'solid-js/store'
interface Todo {
id: number
text: string
completed: boolean
}
interface AppState {
todos: Todo[]
filter: 'all' | 'active' | 'completed'
user: { name: string; settings: { theme: string } }
}
function TodoApp() {
const [state, setState] = createStore<AppState>({
todos: [],
filter: 'all',
user: { name: 'Alice', settings: { theme: 'dark' } },
})
// Add item
const addTodo = (text: string) => {
setState('todos', todos => [
...todos,
{ id: Date.now(), text, completed: false }
])
}
// Toggle specific item by matching
const toggleTodo = (id: number) => {
setState('todos', todo => todo.id === id, 'completed', c => !c)
}
// Delete item (filter produces new array)
const deleteTodo = (id: number) => {
setState('todos', todos => todos.filter(t => t.id !== id))
}
// Mutable-style updates with produce
const clearCompleted = () => {
setState(produce((s) => {
s.todos = s.todos.filter(t => !t.completed)
}))
}
// Deep nested update
const setTheme = (theme: string) => {
setState('user', 'settings', 'theme', theme)
}
// Replace entire store data (reconcile diffs efficiently)
const loadFromServer = async () => {
const data = await fetch('/api/todos').then(r => r.json())
setState('todos', reconcile(data))
}
return (/* ... */)
}
Resource & Data Fetching
Section titled “Resource & Data Fetching”Resource Operations
Section titled “Resource Operations”| Command | Description |
|---|---|
const [data] = createResource(fetchFn) | Create async resource |
const [data] = createResource(id, fetchFn) | Resource with reactive source signal |
data() | Access resource data |
data.loading | Check if resource is loading |
data.error | Access resource error |
data.latest | Get last resolved value (survives refetch) |
data.state | Get resource state (‘unresolved’, ‘pending’, ‘ready’, ‘errored’) |
const { refetch } = data | Manually trigger refetch |
createResource(source, fetcher, { initialValue }) | Resource with initial value |
Data Fetching Patterns
Section titled “Data Fetching Patterns”import { createResource, createSignal, Suspense, ErrorBoundary } from 'solid-js'
interface User {
id: number
name: string
email: string
}
async function fetchUser(id: number): Promise<User> {
const res = await fetch(`/api/users/${id}`)
if (!res.ok) throw new Error(`User ${id} not found`)
return res.json()
}
function UserProfile() {
const [userId, setUserId] = createSignal(1)
// Resource refetches automatically when userId() changes
const [user, { refetch, mutate }] = createResource(userId, fetchUser)
// Optimistic update
const updateName = async (newName: string) => {
const prev = user()
mutate({ ...prev!, name: newName }) // optimistic
try {
await fetch(`/api/users/${userId()}`, {
method: 'PATCH',
body: JSON.stringify({ name: newName }),
})
} catch {
mutate(prev) // rollback on error
}
}
return (
<ErrorBoundary fallback={<p>Failed to load user</p>}>
<Suspense fallback={<p>Loading user...</p>}>
<div>
<h2>{user()?.name}</h2>
<p>{user()?.email}</p>
<p>State: {user.state}</p>
<button onClick={refetch} disabled={user.loading}>
Refresh
</button>
<button onClick={() => setUserId(id => id + 1)}>
Next User
</button>
</div>
</Suspense>
</ErrorBoundary>
)
}
Routing
Section titled “Routing”@solidjs/router
Section titled “@solidjs/router”| Command | Description |
|---|---|
npm install @solidjs/router | Install SolidJS router |
<Router><Route path="/" component={Home}/></Router> | Define basic route |
<Route path="/users/:id" component={User}/> | Route with parameter |
<Route path="/*all" component={NotFound}/> | Catch-all route |
const params = useParams() | Access route parameters |
const [searchParams, setSearchParams] = useSearchParams() | Access query parameters |
const navigate = useNavigate() | Programmatic navigation |
navigate('/dashboard') | Navigate to path |
<A href="/about">About</A> | Navigation link component |
<A href="/about" activeClass="active"> | Link with active styling |
Router Configuration
Section titled “Router Configuration”import { Router, Route, A, useParams, useNavigate, useSearchParams } from '@solidjs/router'
import { lazy } from 'solid-js'
// Lazy-loaded routes
const Dashboard = lazy(() => import('./pages/Dashboard'))
const UserProfile = lazy(() => import('./pages/UserProfile'))
const Settings = lazy(() => import('./pages/Settings'))
function App() {
return (
<Router>
<nav>
<A href="/" activeClass="active" end>Home</A>
<A href="/dashboard" activeClass="active">Dashboard</A>
<A href="/settings" activeClass="active">Settings</A>
</nav>
<Route path="/" component={Home} />
<Route path="/dashboard" component={Dashboard} />
<Route path="/users/:id" component={UserProfile} />
<Route path="/settings" component={Settings} />
<Route path="/*all" component={NotFound} />
</Router>
)
}
// Nested routes
function App() {
return (
<Router>
<Route path="/admin" component={AdminLayout}>
<Route path="/" component={AdminDashboard} />
<Route path="/users" component={AdminUsers} />
<Route path="/users/:id" component={AdminUserDetail} />
</Route>
</Router>
)
}
Context & Dependency Injection
Section titled “Context & Dependency Injection”Context API
Section titled “Context API”import { createContext, useContext, type ParentComponent } from 'solid-js'
import { createStore } from 'solid-js/store'
interface AuthState {
user: { name: string; role: string } | null
token: string | null
}
interface AuthContextValue {
state: AuthState
login: (token: string, user: AuthState['user']) => void
logout: () => void
isAuthenticated: () => boolean
}
const AuthContext = createContext<AuthContextValue>()
const AuthProvider: ParentComponent = (props) => {
const [state, setState] = createStore<AuthState>({
user: null,
token: null,
})
const value: AuthContextValue = {
state,
login: (token, user) => {
setState({ token, user })
},
logout: () => {
setState({ token: null, user: null })
},
isAuthenticated: () => state.token !== null,
}
return (
<AuthContext.Provider value={value}>
{props.children}
</AuthContext.Provider>
)
}
function useAuth() {
const context = useContext(AuthContext)
if (!context) throw new Error('useAuth must be used within AuthProvider')
return context
}
// Usage
function UserMenu() {
const auth = useAuth()
return (
<Show when={auth.isAuthenticated()} fallback={<LoginButton />}>
<p>Hello, {auth.state.user?.name}</p>
<button onClick={auth.logout}>Logout</button>
</Show>
)
}
Styling
Section titled “Styling”CSS Options
Section titled “CSS Options”| Command | Description |
|---|---|
import styles from './App.module.css' | Import CSS modules |
<div class={styles.container}> | Apply CSS module class |
npm install solid-styled-components | Install styled-components for Solid |
const Btn = styled('button')\color: red“ | Create styled component |
<div class="static-class"> | Apply static CSS class |
Tailwind CSS Integration
Section titled “Tailwind CSS Integration”# Install Tailwind CSS
npm install -D tailwindcss postcss autoprefixer
npx tailwindcss init -p
// tailwind.config.js
export default {
content: ['./src/**/*.{js,jsx,ts,tsx}'],
theme: { extend: {} },
plugins: [],
}
/* src/index.css */
@tailwind base;
@tailwind components;
@tailwind utilities;
SolidStart (Meta-Framework)
Section titled “SolidStart (Meta-Framework)”SolidStart Features
Section titled “SolidStart Features”| Command | Description |
|---|---|
npm create solid@latest | Create SolidStart project |
"use server" directive | Mark function as server-only |
createServerAction$() | Create a server action |
createRouteData() | Load data for a route on the server |
<Title>Page Title</Title> | Set page title (from @solidjs/meta) |
<Meta name="description" content="..." /> | Set meta tags |
Server Functions & Actions
Section titled “Server Functions & Actions”// src/routes/todos.tsx
import { createAsync, query, action, redirect } from '@solidjs/router'
// Server query (data loading)
const getTodos = query(async () => {
'use server'
const db = await getDatabase()
return db.todos.findMany()
}, 'todos')
// Server action (mutations)
const addTodo = action(async (formData: FormData) => {
'use server'
const text = formData.get('text') as string
const db = await getDatabase()
await db.todos.create({ data: { text, completed: false } })
throw redirect('/todos') // revalidates
})
export default function TodosPage() {
const todos = createAsync(() => getTodos())
return (
<div>
<form action={addTodo} method="post">
<input name="text" placeholder="New todo" required />
<button type="submit">Add</button>
</form>
<Suspense fallback={<p>Loading...</p>}>
<For each={todos()}>
{(todo) => <p>{todo.text}</p>}
</For>
</Suspense>
</div>
)
}
Testing
Section titled “Testing”Vitest Setup
Section titled “Vitest Setup”npm install -D vitest @solidjs/testing-library @testing-library/jest-dom jsdom
npm install -D vite-plugin-solid
// vitest.config.ts
import { defineConfig } from 'vitest/config'
import solid from 'vite-plugin-solid'
export default defineConfig({
plugins: [solid()],
test: {
environment: 'jsdom',
globals: true,
setupFiles: './src/test-setup.ts',
transformMode: { web: [/\.[jt]sx?$/] },
},
})
Testing Components
Section titled “Testing Components”import { render, screen, fireEvent } from '@solidjs/testing-library'
import { describe, it, expect } from 'vitest'
import Counter from './Counter'
describe('Counter', () => {
it('renders initial count', () => {
render(() => <Counter initialCount={5} />)
expect(screen.getByText('Count: 5')).toBeInTheDocument()
})
it('increments on click', async () => {
render(() => <Counter initialCount={0} />)
const button = screen.getByRole('button', { name: /increment/i })
fireEvent.click(button)
expect(screen.getByText('Count: 1')).toBeInTheDocument()
})
})
Best Practices
Section titled “Best Practices”-
Always call signals as functions —
count()notcount. Forgetting the parentheses is the most common SolidJS mistake; without them you pass the getter function instead of the value. -
Don’t destructure props — destructuring breaks reactivity because it reads the value once. Use
props.namedirectly or usesplitProps()/mergeProps()for prop manipulation. -
Use
<For>for object arrays,<Index>for primitives —<For>keys by reference (better for objects that may reorder) while<Index>keys by position (better for simple value lists). -
Prefer stores for complex state — signals are great for simple values, but for nested objects and arrays,
createStoreprovides granular reactivity without replacing the whole object. -
Use
on()for explicit tracking — when you want an effect to track only specific signals (not every signal read inside), wrap withon(signal, callback). -
Leverage
SuspenseandErrorBoundary— wrap async components inSuspensefor loading states andErrorBoundaryfor graceful error handling. -
Lazy-load routes — use
lazy(() => import('./Page'))for route components to enable code splitting and reduce initial bundle size. -
Use
batch()for multiple updates — when setting several signals at once, wrap them inbatch()to trigger a single re-render instead of multiple. -
Keep components small — SolidJS components run their body only once (not on every render like React), so split large components to limit the scope of reactive updates.
-
Use TypeScript generics with signals —
createSignal<string | null>(null)provides better type safety and autocomplete for your reactive state. -
Understand the compilation model — SolidJS compiles JSX to real DOM operations at build time. There is no virtual DOM diffing, which is why it’s fast but also why the reactivity rules (no destructuring, call signals) matter.
-
Use
untrack()sparingly — reading a signal insideuntrack()prevents dependency tracking. This is useful for logging or one-time reads but can cause bugs if overused.