Skip to content

SolidJS

Fine-grained reactive JavaScript UI framework with no virtual DOM and exceptional performance.

CommandDescription
npx degit solidjs/templates/ts my-appCreate TypeScript SolidJS project
npx degit solidjs/templates/js my-appCreate JavaScript SolidJS project
npm create solid@latestCreate project with SolidStart CLI
npm install solid-jsInstall SolidJS in existing project
npm install solid-startInstall SolidStart meta-framework
CommandDescription
npm run devStart development server with hot reload
npm run buildBuild for production
npm run servePreview production build locally
npm run startStart SolidStart server
# 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
CommandDescription
function App() { return <div>Hello</div> }Define a basic component
<MyComponent name="value" />Pass props to a component
props.childrenAccess 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
CommandDescription
<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
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>
}
CommandDescription
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
CommandDescription
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
CommandDescription
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
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>
  )
}
CommandDescription
<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
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>
  )
}
CommandDescription
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
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 (/* ... */)
}
CommandDescription
const [data] = createResource(fetchFn)Create async resource
const [data] = createResource(id, fetchFn)Resource with reactive source signal
data()Access resource data
data.loadingCheck if resource is loading
data.errorAccess resource error
data.latestGet last resolved value (survives refetch)
data.stateGet resource state (‘unresolved’, ‘pending’, ‘ready’, ‘errored’)
const { refetch } = dataManually trigger refetch
createResource(source, fetcher, { initialValue })Resource with initial value
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>
  )
}
CommandDescription
npm install @solidjs/routerInstall 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
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>
  )
}
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>
  )
}
CommandDescription
import styles from './App.module.css'Import CSS modules
<div class={styles.container}>Apply CSS module class
npm install solid-styled-componentsInstall styled-components for Solid
const Btn = styled('button')\color: red“Create styled component
<div class="static-class">Apply static CSS class
# 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;
CommandDescription
npm create solid@latestCreate SolidStart project
"use server" directiveMark 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
// 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>
  )
}
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?$/] },
  },
})
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()
  })
})
  1. Always call signals as functionscount() not count. Forgetting the parentheses is the most common SolidJS mistake; without them you pass the getter function instead of the value.

  2. Don’t destructure props — destructuring breaks reactivity because it reads the value once. Use props.name directly or use splitProps()/mergeProps() for prop manipulation.

  3. 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).

  4. Prefer stores for complex state — signals are great for simple values, but for nested objects and arrays, createStore provides granular reactivity without replacing the whole object.

  5. Use on() for explicit tracking — when you want an effect to track only specific signals (not every signal read inside), wrap with on(signal, callback).

  6. Leverage Suspense and ErrorBoundary — wrap async components in Suspense for loading states and ErrorBoundary for graceful error handling.

  7. Lazy-load routes — use lazy(() => import('./Page')) for route components to enable code splitting and reduce initial bundle size.

  8. Use batch() for multiple updates — when setting several signals at once, wrap them in batch() to trigger a single re-render instead of multiple.

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

  10. Use TypeScript generics with signalscreateSignal<string | null>(null) provides better type safety and autocomplete for your reactive state.

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

  12. Use untrack() sparingly — reading a signal inside untrack() prevents dependency tracking. This is useful for logging or one-time reads but can cause bugs if overused.