We use cookies to enhance your experience on the site
CodeWorlds
Back to collections
Guide33 min read

SolidJS

SolidJS is a reactive framework compiling to vanilla JS with fine-grained reactivity and best-in-class performance.

SolidJS - True Reactivity Without Virtual DOM

Czym jest SolidJS?

SolidJS to nowoczesny framework JavaScript stworzony przez Ryana Carniato, który łączy znajomą składnię JSX (podobną do React) z prawdziwą reaktywnością na poziomie fine-grained. W przeciwieństwie do React, SolidJS nie używa Virtual DOM - komponenty kompilują się do optymalnych operacji DOM, co czyni go jednym z najszybszych frameworków w benchmarkach.

Framework wprowadza koncepcję Signals jako podstawową prymitywę reaktywności, która automatycznie śledzi zależności i aktualizuje tylko te części DOM, które faktycznie się zmieniły. To podejście eliminuje re-rendery całych komponentów i zapewnia wydajność porównywalną z vanilla JavaScript.

Dlaczego SolidJS?

Kluczowe zalety

  1. Najwyższa wydajność - Regularnie wygrywa w benchmarkach JS Framework Benchmark
  2. Fine-grained reactivity - Aktualizacje tylko tam, gdzie są potrzebne
  3. Zero Virtual DOM - Bezpośrednie operacje DOM
  4. Znajoma składnia JSX - Łatwe przejście z React
  5. Małe bundle - ~7KB gzipped dla core library
  6. Prosty model mentalny - Komponenty wykonują się raz

SolidJS vs React vs Vue vs Svelte

CechaSolidJSReactVue 3Svelte
Virtual DOMNieTakTakNie
ReaktywnośćFine-grainedComponent-levelFine-grainedCompile-time
Bundle size~7KB~45KB~35KB~2KB
PerformanceNajszybszyDobraBardzo dobraŚwietna
JSXNatywnyNatywnyOpcjonalnyWłasna składnia
Learning curveNiska (dla React devs)ŚredniaŚredniaNiska
SSRSolidStartNext.jsNuxtSvelteKit
TypeScriptŚwietnyŚwietnyŚwietnyDobry

Instalacja i konfiguracja

Tworzenie nowego projektu

Code
Bash
# Z Vite template
npm create solid@latest my-app

# Interaktywny wizard zapyta o:
# - JavaScript/TypeScript
# - Solid/SolidStart (pełny framework)
# - Template (bare/todoMVC/hackernews)

cd my-app
npm install
npm run dev

Struktura projektu SolidStart

Code
TEXT
my-solid-app/
├── src/
│   ├── components/           # Komponenty
│   │   ├── Counter.tsx
│   │   └── Header.tsx
│   ├── routes/               # File-based routing
│   │   ├── index.tsx         # /
│   │   ├── about.tsx         # /about
│   │   └── blog/
│   │       ├── index.tsx     # /blog
│   │       └── [slug].tsx    # /blog/:slug
│   ├── lib/                  # Utilities
│   ├── entry-client.tsx      # Client entry
│   ├── entry-server.tsx      # Server entry
│   └── root.tsx              # Root layout
├── public/                   # Static assets
├── vite.config.ts            # Vite config
├── app.config.ts             # SolidStart config
└── package.json

Podstawowa konfiguracja

TSvite.config.ts
TypeScript
// vite.config.ts
import { defineConfig } from 'vite'
import solid from 'vite-plugin-solid'

export default defineConfig({
  plugins: [solid()],
})
TSapp.config.ts
TypeScript
// app.config.ts (SolidStart)
import { defineConfig } from '@solidjs/start/config'

export default defineConfig({
  vite: {
    // Vite options
  },
  server: {
    preset: 'vercel', // lub 'node', 'cloudflare-pages', 'netlify'
  },
})

Podstawy SolidJS

Signals - reaktywny stan

Code
TypeScript
import { createSignal } from 'solid-js'

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

  // WAŻNE: count() - signal to funkcja!
  // W React: count, W SolidJS: count()

  return (
    <div>
      <p>Count: {count()}</p>

      <button onClick={() => setCount(count() + 1)}>
        Increment
      </button>

      <button onClick={() => setCount(c => c - 1)}>
        Decrement
      </button>

      <button onClick={() => setCount(0)}>
        Reset
      </button>
    </div>
  )
}

Derived state (bez createMemo)

Code
TypeScript
import { createSignal } from 'solid-js'

function Example() {
  const [firstName, setFirstName] = createSignal('John')
  const [lastName, setLastName] = createSignal('Doe')

  // Derived state - prosta funkcja
  // Automatycznie reaktywna dzięki fine-grained tracking
  const fullName = () => `${firstName()} ${lastName()}`

  return (
    <div>
      <input
        value={firstName()}
        onInput={(e) => setFirstName(e.target.value)}
      />
      <input
        value={lastName()}
        onInput={(e) => setLastName(e.target.value)}
      />
      <p>Full name: {fullName()}</p>
    </div>
  )
}

createMemo - cached computed

Code
TypeScript
import { createSignal, createMemo } from 'solid-js'

function ExpensiveComputation() {
  const [items, setItems] = createSignal([1, 2, 3, 4, 5])
  const [filter, setFilter] = createSignal('')

  // createMemo cache'uje wynik - nie przelicza się przy każdym renderze
  const expensiveSum = createMemo(() => {
    console.log('Computing sum...')
    return items().reduce((sum, item) => sum + item, 0)
  })

  const filteredItems = createMemo(() => {
    const query = filter().toLowerCase()
    return items().filter(item =>
      String(item).toLowerCase().includes(query)
    )
  })

  return (
    <div>
      <p>Sum: {expensiveSum()}</p>
      <p>Filtered: {filteredItems().join(', ')}</p>

      <input
        placeholder="Filter..."
        onInput={(e) => setFilter(e.target.value)}
      />

      <button onClick={() => setItems([...items(), items().length + 1])}>
        Add Item
      </button>
    </div>
  )
}

createEffect - side effects

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

function EffectExample() {
  const [searchQuery, setSearchQuery] = createSignal('')
  const [results, setResults] = createSignal([])

  // createEffect automatycznie trackuje dependencies
  createEffect(() => {
    const query = searchQuery()

    if (query.length < 3) {
      setResults([])
      return
    }

    // Debounce
    const timeoutId = setTimeout(async () => {
      const response = await fetch(`/api/search?q=${query}`)
      const data = await response.json()
      setResults(data)
    }, 300)

    // Cleanup - wywoływane przy każdej zmianie lub unmount
    onCleanup(() => clearTimeout(timeoutId))
  })

  // Effect z console log dla debugowania
  createEffect(() => {
    console.log('Search query changed:', searchQuery())
  })

  return (
    <div>
      <input
        placeholder="Search..."
        onInput={(e) => setSearchQuery(e.target.value)}
      />
      <ul>
        <For each={results()}>
          {(result) => <li>{result}</li>}
        </For>
      </ul>
    </div>
  )
}

onMount i onCleanup

Code
TypeScript
import { createSignal, onMount, onCleanup } from 'solid-js'

function WindowSizeTracker() {
  const [size, setSize] = createSignal({ width: 0, height: 0 })

  // onMount - wykonuje się raz po zamontowaniu
  onMount(() => {
    // Dostęp do DOM i browser APIs
    setSize({
      width: window.innerWidth,
      height: window.innerHeight,
    })

    const handleResize = () => {
      setSize({
        width: window.innerWidth,
        height: window.innerHeight,
      })
    }

    window.addEventListener('resize', handleResize)

    // onCleanup - cleanup przy unmount
    onCleanup(() => {
      window.removeEventListener('resize', handleResize)
    })
  })

  return (
    <div>
      <p>Window: {size().width} x {size().height}</p>
    </div>
  )
}

Control Flow Components

SolidJS używa komponentów do control flow zamiast JSX conditionals - to kluczowe dla fine-grained updates.

Show - warunkowe renderowanie

Code
TypeScript
import { Show, createSignal } from 'solid-js'

function ConditionalExample() {
  const [isLoggedIn, setIsLoggedIn] = createSignal(false)
  const [user, setUser] = createSignal(null)

  return (
    <div>
      {/* Podstawowe Show */}
      <Show when={isLoggedIn()}>
        <Dashboard />
      </Show>

      {/* Show z fallback */}
      <Show when={isLoggedIn()} fallback={<LoginForm />}>
        <Dashboard />
      </Show>

      {/* Show z keyed - user przekazany do children */}
      <Show when={user()} keyed>
        {(userData) => (
          <div>
            <h1>Welcome, {userData.name}</h1>
            <p>Email: {userData.email}</p>
          </div>
        )}
      </Show>
    </div>
  )
}

For - iteracja po listach

Code
TypeScript
import { For, createSignal } from 'solid-js'

interface Todo {
  id: number
  text: string
  completed: boolean
}

function TodoList() {
  const [todos, setTodos] = createSignal<Todo[]>([
    { id: 1, text: 'Learn Solid', completed: false },
    { id: 2, text: 'Build app', completed: false },
  ])

  const toggleTodo = (id: number) => {
    setTodos(todos =>
      todos.map(todo =>
        todo.id === id
          ? { ...todo, completed: !todo.completed }
          : todo
      )
    )
  }

  const removeTodo = (id: number) => {
    setTodos(todos => todos.filter(todo => todo.id !== id))
  }

  return (
    <ul>
      {/* For - wydajne iterowanie z automatycznym key */}
      <For each={todos()}>
        {(todo, index) => (
          <li>
            <input
              type="checkbox"
              checked={todo.completed}
              onChange={() => toggleTodo(todo.id)}
            />
            <span class={todo.completed ? 'completed' : ''}>
              {index() + 1}. {todo.text}
            </span>
            <button onClick={() => removeTodo(todo.id)}>×</button>
          </li>
        )}
      </For>
    </ul>
  )
}

Index - iteracja z stabilnym index

Code
TypeScript
import { Index, createSignal } from 'solid-js'

function IndexExample() {
  const [items, setItems] = createSignal(['A', 'B', 'C', 'D'])

  // Index - index jest stały, wartość może się zmieniać
  // For - wartość jest stała, index może się zmieniać

  return (
    <ul>
      <Index each={items()}>
        {(item, index) => (
          <li>
            {/* index to number, item() to signal */}
            {index}: {item()}
            <input
              value={item()}
              onInput={(e) => {
                const newItems = [...items()]
                newItems[index] = e.target.value
                setItems(newItems)
              }}
            />
          </li>
        )}
      </Index>
    </ul>
  )
}

Switch/Match - pattern matching

Code
TypeScript
import { Switch, Match, createSignal } from 'solid-js'

function StatusDisplay() {
  const [status, setStatus] = createSignal<'idle' | 'loading' | 'success' | 'error'>('idle')
  const [data, setData] = createSignal(null)
  const [error, setError] = createSignal(null)

  return (
    <div>
      <Switch fallback={<p>Unknown status</p>}>
        <Match when={status() === 'idle'}>
          <p>Click to load data</p>
        </Match>

        <Match when={status() === 'loading'}>
          <div class="spinner">Loading...</div>
        </Match>

        <Match when={status() === 'error'}>
          <div class="error">
            Error: {error()?.message}
            <button onClick={() => setStatus('idle')}>Retry</button>
          </div>
        </Match>

        <Match when={status() === 'success'}>
          <div class="success">
            <pre>{JSON.stringify(data(), null, 2)}</pre>
          </div>
        </Match>
      </Switch>
    </div>
  )
}

Dynamic - dynamiczny komponent

Code
TypeScript
import { Dynamic, createSignal } from 'solid-js'

// Komponenty
const RedBox = () => <div style={{ background: 'red', padding: '20px' }}>Red</div>
const BlueBox = () => <div style={{ background: 'blue', padding: '20px' }}>Blue</div>
const GreenBox = () => <div style={{ background: 'green', padding: '20px' }}>Green</div>

function DynamicComponentExample() {
  const [selected, setSelected] = createSignal<'red' | 'blue' | 'green'>('red')

  const components = {
    red: RedBox,
    blue: BlueBox,
    green: GreenBox,
  }

  return (
    <div>
      <select onChange={(e) => setSelected(e.target.value as any)}>
        <option value="red">Red</option>
        <option value="blue">Blue</option>
        <option value="green">Green</option>
      </select>

      {/* Dynamic renderuje wybrany komponent */}
      <Dynamic component={components[selected()]} />
    </div>
  )
}

Portal - renderowanie poza drzewem

Code
TypeScript
import { Portal, createSignal } from 'solid-js'

function ModalExample() {
  const [isOpen, setIsOpen] = createSignal(false)

  return (
    <div>
      <button onClick={() => setIsOpen(true)}>Open Modal</button>

      <Show when={isOpen()}>
        {/* Portal renderuje children do document.body */}
        <Portal>
          <div class="modal-overlay" onClick={() => setIsOpen(false)}>
            <div class="modal" onClick={(e) => e.stopPropagation()}>
              <h2>Modal Title</h2>
              <p>Modal content here</p>
              <button onClick={() => setIsOpen(false)}>Close</button>
            </div>
          </div>
        </Portal>
      </Show>
    </div>
  )
}

Stores - zarządzanie stanem

createStore - nested reactive state

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

interface User {
  id: number
  name: string
  email: string
  settings: {
    theme: 'light' | 'dark'
    notifications: boolean
  }
}

interface AppState {
  user: User | null
  todos: { id: number; text: string; completed: boolean }[]
  isLoading: boolean
}

function AppWithStore() {
  // createStore dla złożonych obiektów
  const [state, setState] = createStore<AppState>({
    user: null,
    todos: [],
    isLoading: false,
  })

  // Aktualizacja nested property - path syntax
  const setTheme = (theme: 'light' | 'dark') => {
    setState('user', 'settings', 'theme', theme)
  }

  // Aktualizacja z produce (immer-like)
  const addTodo = (text: string) => {
    setState(produce((s) => {
      s.todos.push({
        id: Date.now(),
        text,
        completed: false,
      })
    }))
  }

  const toggleTodo = (id: number) => {
    setState('todos', (todo) => todo.id === id, 'completed', (c) => !c)
  }

  // Login
  const login = async (email: string, password: string) => {
    setState('isLoading', true)

    try {
      const response = await fetch('/api/login', {
        method: 'POST',
        body: JSON.stringify({ email, password }),
      })
      const user = await response.json()
      setState('user', user)
    } finally {
      setState('isLoading', false)
    }
  }

  return (
    <div>
      <Show when={state.user} fallback={<LoginForm onLogin={login} />}>
        <p>Welcome, {state.user!.name}</p>

        <button onClick={() => setTheme(
          state.user!.settings.theme === 'light' ? 'dark' : 'light'
        )}>
          Toggle theme: {state.user!.settings.theme}
        </button>

        <For each={state.todos}>
          {(todo) => (
            <div>
              <input
                type="checkbox"
                checked={todo.completed}
                onChange={() => toggleTodo(todo.id)}
              />
              {todo.text}
            </div>
          )}
        </For>
      </Show>
    </div>
  )
}

reconcile - efektywna aktualizacja

Code
TypeScript
import { createStore, reconcile } from 'solid-js/store'

function DataGrid() {
  const [data, setData] = createStore<{ items: Item[] }>({ items: [] })

  const fetchData = async () => {
    const response = await fetch('/api/items')
    const newItems = await response.json()

    // reconcile efektywnie porównuje i aktualizuje
    // zamiast zastępować całą tablicę
    setData('items', reconcile(newItems))
  }

  return (
    <div>
      <button onClick={fetchData}>Refresh</button>
      <For each={data.items}>
        {(item) => <ItemRow item={item} />}
      </For>
    </div>
  )
}

Context API

Code
TypeScript
import { createContext, useContext, ParentComponent } from 'solid-js'
import { createStore } from 'solid-js/store'

// Definicja typu context
interface AuthContextValue {
  user: () => User | null
  login: (email: string, password: string) => Promise<void>
  logout: () => void
  isLoading: () => boolean
}

// Tworzenie context
const AuthContext = createContext<AuthContextValue>()

// Provider
export const AuthProvider: ParentComponent = (props) => {
  const [state, setState] = createStore({
    user: null as User | null,
    isLoading: false,
  })

  const login = async (email: string, password: string) => {
    setState('isLoading', true)
    try {
      const response = await fetch('/api/auth/login', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ email, password }),
      })
      const user = await response.json()
      setState('user', user)
    } finally {
      setState('isLoading', false)
    }
  }

  const logout = () => {
    setState('user', null)
  }

  const value: AuthContextValue = {
    user: () => state.user,
    login,
    logout,
    isLoading: () => state.isLoading,
  }

  return (
    <AuthContext.Provider value={value}>
      {props.children}
    </AuthContext.Provider>
  )
}

// Hook do używania context
export function useAuth() {
  const context = useContext(AuthContext)
  if (!context) {
    throw new Error('useAuth must be used within AuthProvider')
  }
  return context
}

// Użycie
function Profile() {
  const { user, logout } = useAuth()

  return (
    <Show when={user()}>
      <div>
        <p>Welcome, {user()!.name}</p>
        <button onClick={logout}>Logout</button>
      </div>
    </Show>
  )
}

// App
function App() {
  return (
    <AuthProvider>
      <Header />
      <Profile />
    </AuthProvider>
  )
}

Resources - async data

Code
TypeScript
import { createResource, Suspense, ErrorBoundary, Show } from 'solid-js'

// createResource dla async data
function UserProfile(props: { userId: string }) {
  // Pierwszy argument: source signal
  // Drugi argument: fetcher function
  const [user, { mutate, refetch }] = createResource(
    () => props.userId,
    async (userId) => {
      const response = await fetch(`/api/users/${userId}`)
      if (!response.ok) throw new Error('User not found')
      return response.json()
    }
  )

  return (
    <div>
      <Show when={user.loading}>
        <p>Loading...</p>
      </Show>

      <Show when={user.error}>
        <p>Error: {user.error.message}</p>
        <button onClick={refetch}>Retry</button>
      </Show>

      <Show when={user()}>
        <div>
          <h2>{user()!.name}</h2>
          <p>{user()!.email}</p>

          {/* Optimistic update */}
          <button onClick={() => {
            mutate({ ...user()!, name: 'Updated Name' })
            // Następnie sync z serwerem
          }}>
            Update Name
          </button>
        </div>
      </Show>
    </div>
  )
}

// Z Suspense
function App() {
  const [userId, setUserId] = createSignal('1')

  return (
    <ErrorBoundary fallback={(err) => <p>Error: {err.message}</p>}>
      <Suspense fallback={<div>Loading user...</div>}>
        <UserProfile userId={userId()} />
      </Suspense>
    </ErrorBoundary>
  )
}

SolidStart - Meta-Framework

Routing

TSsrc/routes/index.tsx
TypeScript
// src/routes/index.tsx
export default function Home() {
  return (
    <main>
      <h1>Welcome to SolidStart</h1>
    </main>
  )
}
TSsrc/routes/blog/[slug].tsx
TypeScript
// src/routes/blog/[slug].tsx
import { useParams } from '@solidjs/router'
import { createAsync, cache } from '@solidjs/router'

// Cache dla data fetching
const getPost = cache(async (slug: string) => {
  const response = await fetch(`/api/posts/${slug}`)
  return response.json()
}, 'post')

export default function BlogPost() {
  const params = useParams()

  // createAsync - suspense-enabled data fetching
  const post = createAsync(() => getPost(params.slug))

  return (
    <article>
      <h1>{post()?.title}</h1>
      <div innerHTML={post()?.content} />
    </article>
  )
}

Server Functions

TSsrc/routes/api/users.ts
TypeScript
// src/routes/api/users.ts
import { json } from '@solidjs/router'

export async function GET() {
  const users = await db.users.findMany()
  return json(users)
}

export async function POST({ request }) {
  const body = await request.json()
  const user = await db.users.create({ data: body })
  return json(user, { status: 201 })
}
Code
TypeScript
// Server actions w komponentach
import { action, useAction, useSubmission } from '@solidjs/router'

// Definicja server action
const addTodo = action(async (formData: FormData) => {
  'use server'
  const text = formData.get('text') as string
  await db.todos.create({ data: { text } })
}, 'addTodo')

function TodoForm() {
  const submission = useSubmission(addTodo)

  return (
    <form action={addTodo} method="post">
      <input name="text" placeholder="New todo..." disabled={submission.pending} />
      <button type="submit" disabled={submission.pending}>
        {submission.pending ? 'Adding...' : 'Add'}
      </button>
    </form>
  )
}

Layouts

TSsrc/routes/(app).tsx
TypeScript
// src/routes/(app).tsx - Layout dla route group
import { ParentComponent } from 'solid-js'

const AppLayout: ParentComponent = (props) => {
  return (
    <div class="app-layout">
      <Header />
      <nav>
        <A href="/dashboard">Dashboard</A>
        <A href="/settings">Settings</A>
      </nav>
      <main>
        {props.children}
      </main>
      <Footer />
    </div>
  )
}

export default AppLayout

Komponenty - wzorce

Props z defaults

Code
TypeScript
import { Component, mergeProps, splitProps } from 'solid-js'

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

const Button: Component<ButtonProps> = (props) => {
  // mergeProps dla defaultów
  const merged = mergeProps(
    { variant: 'primary', size: 'md', disabled: false },
    props
  )

  // splitProps rozdziela props
  const [local, others] = splitProps(merged, [
    'variant', 'size', 'disabled', 'class', 'children'
  ])

  const classes = () =>
    `btn btn-${local.variant} btn-${local.size} ${local.class || ''}`

  return (
    <button
      class={classes()}
      disabled={local.disabled}
      {...others}
    >
      {local.children}
    </button>
  )
}

Ref forwarding

Code
TypeScript
import { Component, onMount } from 'solid-js'

interface InputProps {
  ref?: HTMLInputElement | ((el: HTMLInputElement) => void)
  placeholder?: string
}

const Input: Component<InputProps> = (props) => {
  let inputRef: HTMLInputElement

  onMount(() => {
    // Forward ref
    if (typeof props.ref === 'function') {
      props.ref(inputRef)
    }
  })

  return (
    <input
      ref={inputRef!}
      placeholder={props.placeholder}
      class="input"
    />
  )
}

// Użycie
function Form() {
  let inputRef: HTMLInputElement

  onMount(() => {
    inputRef.focus()
  })

  return (
    <form>
      <Input ref={inputRef!} placeholder="Focus on mount" />
    </form>
  )
}

Children handling

Code
TypeScript
import { ParentComponent, children, JSX } from 'solid-js'

interface CardProps {
  title?: string
  children: JSX.Element
}

const Card: ParentComponent<CardProps> = (props) => {
  // children() helper dla transformacji children
  const resolved = children(() => props.children)

  return (
    <div class="card">
      <Show when={props.title}>
        <div class="card-header">{props.title}</div>
      </Show>
      <div class="card-body">
        {resolved()}
      </div>
    </div>
  )
}

Integracje

Tailwind CSS

Code
Bash
npm install -D tailwindcss postcss autoprefixer
npx tailwindcss init -p
JStailwind.config.js
JavaScript
// tailwind.config.js
module.exports = {
  content: ['./src/**/*.{js,jsx,ts,tsx}'],
  theme: { extend: {} },
  plugins: [],
}

React Hook Form (lub własne)

Code
TypeScript
import { createSignal, createEffect } from 'solid-js'
import { createStore } from 'solid-js/store'

// Prosty form hook dla Solid
function createForm<T extends Record<string, any>>(initialValues: T) {
  const [values, setValues] = createStore(initialValues)
  const [errors, setErrors] = createStore<Partial<Record<keyof T, string>>>({})
  const [touched, setTouched] = createStore<Partial<Record<keyof T, boolean>>>({})

  const setValue = (field: keyof T, value: any) => {
    setValues(field as any, value)
  }

  const setError = (field: keyof T, error: string) => {
    setErrors(field as any, error)
  }

  const setFieldTouched = (field: keyof T) => {
    setTouched(field as any, true)
  }

  const reset = () => {
    setValues(initialValues)
    setErrors({})
    setTouched({})
  }

  return {
    values,
    errors,
    touched,
    setValue,
    setError,
    setFieldTouched,
    reset,
  }
}

// Użycie
function ContactForm() {
  const form = createForm({
    name: '',
    email: '',
    message: '',
  })

  const handleSubmit = async (e: Event) => {
    e.preventDefault()
    // Walidacja i submit
  }

  return (
    <form onSubmit={handleSubmit}>
      <input
        value={form.values.name}
        onInput={(e) => form.setValue('name', e.target.value)}
        onBlur={() => form.setFieldTouched('name')}
      />
      {form.touched.name && form.errors.name && (
        <span class="error">{form.errors.name}</span>
      )}
      {/* ... */}
    </form>
  )
}

Deployment

Vercel

Code
Bash
npm install @solidjs/start
TSapp.config.ts
TypeScript
// app.config.ts
import { defineConfig } from '@solidjs/start/config'

export default defineConfig({
  server: {
    preset: 'vercel',
  },
})

Cloudflare Pages

TSapp.config.ts
TypeScript
// app.config.ts
export default defineConfig({
  server: {
    preset: 'cloudflare-pages',
  },
})

Static Export

TSapp.config.ts
TypeScript
// app.config.ts
export default defineConfig({
  server: {
    preset: 'static',
    prerender: {
      routes: ['/', '/about', '/blog'],
    },
  },
})

Performance Tips

Code
TypeScript
// 1. Unikaj niepotrzebnych wrapper'ów
// ❌
<div>{count()}</div>

// ✅
<span>{count()}</span>

// 2. Używaj createMemo dla kosztownych obliczeń
const expensive = createMemo(() => {
  return items().filter(/* ... */).map(/* ... */)
})

// 3. Batch updates
import { batch } from 'solid-js'

batch(() => {
  setCount(count() + 1)
  setName('New name')
  setItems([...items(), newItem])
})

// 4. untrack dla odczytu bez tworzenia dependency
import { untrack } from 'solid-js'

createEffect(() => {
  const currentCount = count() // tracked
  const staticValue = untrack(() => someSignal()) // not tracked
})

// 5. Lazy components
import { lazy } from 'solid-js'

const HeavyComponent = lazy(() => import('./HeavyComponent'))

Cennik

  • 100% darmowy - MIT License

FAQ - Często zadawane pytania

Czy mogę używać bibliotek React z SolidJS?

Nie bezpośrednio. SolidJS ma inny model reaktywności. Jednak wiele bibliotek ma odpowiedniki Solid lub możesz napisać wrapper. Społeczność aktywnie tworzy solid-* wersje popularnych bibliotek.

Dlaczego count() zamiast count?

Signals w SolidJS to funkcje. Wywołanie count() odczytuje wartość i rejestruje dependency. To pozwala na fine-grained tracking bez Virtual DOM.

Czy SolidJS nadaje się do dużych aplikacji?

Tak. SolidJS skaluje się doskonale dzięki fine-grained reactivity. Nie ma problemu z re-renderami dużych drzew komponentów jak w React.

Jaka jest krzywa uczenia się?

Dla developerów React jest bardzo niska. Składnia JSX jest prawie identyczna. Główna różnica to używanie count() zamiast count i komponenty control flow (Show, For).

Czy SolidJS ma ekosystem?

Rosnący. Jest SolidStart (meta-framework), solid-ui, solid-primitives (utility hooks), i wiele innych. Społeczność jest mniejsza niż React, ale bardzo aktywna.


SolidJS - True Reactivity Without Virtual DOM

What is SolidJS?

SolidJS is a modern JavaScript framework created by Ryan Carniato that combines familiar JSX syntax (similar to React) with true fine-grained reactivity. Unlike React, SolidJS does not use a Virtual DOM - components compile to optimal DOM operations, making it one of the fastest frameworks in benchmarks.

The framework introduces the concept of Signals as its core reactivity primitive, which automatically tracks dependencies and updates only the parts of the DOM that have actually changed. This approach eliminates re-renders of entire components and delivers performance comparable to vanilla JavaScript.

Why SolidJS?

Key advantages

  1. Best-in-class performance - Regularly wins in JS Framework Benchmark comparisons
  2. Fine-grained reactivity - Updates only where needed
  3. Zero Virtual DOM - Direct DOM operations
  4. Familiar JSX syntax - Easy transition from React
  5. Small bundle - ~7KB gzipped for the core library
  6. Simple mental model - Components execute once

SolidJS vs React vs Vue vs Svelte

FeatureSolidJSReactVue 3Svelte
Virtual DOMNoYesYesNo
ReactivityFine-grainedComponent-levelFine-grainedCompile-time
Bundle size~7KB~45KB~35KB~2KB
PerformanceFastestGoodVery goodGreat
JSXNativeNativeOptionalCustom syntax
Learning curveLow (for React devs)MediumMediumLow
SSRSolidStartNext.jsNuxtSvelteKit
TypeScriptExcellentExcellentExcellentGood

Installation and configuration

Creating a new project

Code
Bash
# With Vite template
npm create solid@latest my-app

# The interactive wizard will ask about:
# - JavaScript/TypeScript
# - Solid/SolidStart (full framework)
# - Template (bare/todoMVC/hackernews)

cd my-app
npm install
npm run dev

SolidStart project structure

Code
TEXT
my-solid-app/
├── src/
│   ├── components/           # Components
│   │   ├── Counter.tsx
│   │   └── Header.tsx
│   ├── routes/               # File-based routing
│   │   ├── index.tsx         # /
│   │   ├── about.tsx         # /about
│   │   └── blog/
│   │       ├── index.tsx     # /blog
│   │       └── [slug].tsx    # /blog/:slug
│   ├── lib/                  # Utilities
│   ├── entry-client.tsx      # Client entry
│   ├── entry-server.tsx      # Server entry
│   └── root.tsx              # Root layout
├── public/                   # Static assets
├── vite.config.ts            # Vite config
├── app.config.ts             # SolidStart config
└── package.json

Basic configuration

TSvite.config.ts
TypeScript
// vite.config.ts
import { defineConfig } from 'vite'
import solid from 'vite-plugin-solid'

export default defineConfig({
  plugins: [solid()],
})
TSapp.config.ts
TypeScript
// app.config.ts (SolidStart)
import { defineConfig } from '@solidjs/start/config'

export default defineConfig({
  vite: {
    // Vite options
  },
  server: {
    preset: 'vercel', // or 'node', 'cloudflare-pages', 'netlify'
  },
})

SolidJS basics

Signals - reactive state

Code
TypeScript
import { createSignal } from 'solid-js'

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

  // IMPORTANT: count() - a signal is a function!
  // In React: count, In SolidJS: count()

  return (
    <div>
      <p>Count: {count()}</p>

      <button onClick={() => setCount(count() + 1)}>
        Increment
      </button>

      <button onClick={() => setCount(c => c - 1)}>
        Decrement
      </button>

      <button onClick={() => setCount(0)}>
        Reset
      </button>
    </div>
  )
}

Derived state (without createMemo)

Code
TypeScript
import { createSignal } from 'solid-js'

function Example() {
  const [firstName, setFirstName] = createSignal('John')
  const [lastName, setLastName] = createSignal('Doe')

  // Derived state - a simple function
  // Automatically reactive thanks to fine-grained tracking
  const fullName = () => `${firstName()} ${lastName()}`

  return (
    <div>
      <input
        value={firstName()}
        onInput={(e) => setFirstName(e.target.value)}
      />
      <input
        value={lastName()}
        onInput={(e) => setLastName(e.target.value)}
      />
      <p>Full name: {fullName()}</p>
    </div>
  )
}

createMemo - cached computed

Code
TypeScript
import { createSignal, createMemo } from 'solid-js'

function ExpensiveComputation() {
  const [items, setItems] = createSignal([1, 2, 3, 4, 5])
  const [filter, setFilter] = createSignal('')

  // createMemo caches the result - it doesn't recompute on every render
  const expensiveSum = createMemo(() => {
    console.log('Computing sum...')
    return items().reduce((sum, item) => sum + item, 0)
  })

  const filteredItems = createMemo(() => {
    const query = filter().toLowerCase()
    return items().filter(item =>
      String(item).toLowerCase().includes(query)
    )
  })

  return (
    <div>
      <p>Sum: {expensiveSum()}</p>
      <p>Filtered: {filteredItems().join(', ')}</p>

      <input
        placeholder="Filter..."
        onInput={(e) => setFilter(e.target.value)}
      />

      <button onClick={() => setItems([...items(), items().length + 1])}>
        Add Item
      </button>
    </div>
  )
}

createEffect - side effects

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

function EffectExample() {
  const [searchQuery, setSearchQuery] = createSignal('')
  const [results, setResults] = createSignal([])

  // createEffect automatically tracks dependencies
  createEffect(() => {
    const query = searchQuery()

    if (query.length < 3) {
      setResults([])
      return
    }

    // Debounce
    const timeoutId = setTimeout(async () => {
      const response = await fetch(`/api/search?q=${query}`)
      const data = await response.json()
      setResults(data)
    }, 300)

    // Cleanup - called on every change or unmount
    onCleanup(() => clearTimeout(timeoutId))
  })

  // Effect with console log for debugging
  createEffect(() => {
    console.log('Search query changed:', searchQuery())
  })

  return (
    <div>
      <input
        placeholder="Search..."
        onInput={(e) => setSearchQuery(e.target.value)}
      />
      <ul>
        <For each={results()}>
          {(result) => <li>{result}</li>}
        </For>
      </ul>
    </div>
  )
}

onMount and onCleanup

Code
TypeScript
import { createSignal, onMount, onCleanup } from 'solid-js'

function WindowSizeTracker() {
  const [size, setSize] = createSignal({ width: 0, height: 0 })

  // onMount - runs once after mounting
  onMount(() => {
    // Access to DOM and browser APIs
    setSize({
      width: window.innerWidth,
      height: window.innerHeight,
    })

    const handleResize = () => {
      setSize({
        width: window.innerWidth,
        height: window.innerHeight,
      })
    }

    window.addEventListener('resize', handleResize)

    // onCleanup - cleanup on unmount
    onCleanup(() => {
      window.removeEventListener('resize', handleResize)
    })
  })

  return (
    <div>
      <p>Window: {size().width} x {size().height}</p>
    </div>
  )
}

Control flow components

SolidJS uses components for control flow instead of JSX conditionals - this is crucial for fine-grained updates.

Show - conditional rendering

Code
TypeScript
import { Show, createSignal } from 'solid-js'

function ConditionalExample() {
  const [isLoggedIn, setIsLoggedIn] = createSignal(false)
  const [user, setUser] = createSignal(null)

  return (
    <div>
      {/* Basic Show */}
      <Show when={isLoggedIn()}>
        <Dashboard />
      </Show>

      {/* Show with fallback */}
      <Show when={isLoggedIn()} fallback={<LoginForm />}>
        <Dashboard />
      </Show>

      {/* Show with keyed - user passed to children */}
      <Show when={user()} keyed>
        {(userData) => (
          <div>
            <h1>Welcome, {userData.name}</h1>
            <p>Email: {userData.email}</p>
          </div>
        )}
      </Show>
    </div>
  )
}

For - iterating over lists

Code
TypeScript
import { For, createSignal } from 'solid-js'

interface Todo {
  id: number
  text: string
  completed: boolean
}

function TodoList() {
  const [todos, setTodos] = createSignal<Todo[]>([
    { id: 1, text: 'Learn Solid', completed: false },
    { id: 2, text: 'Build app', completed: false },
  ])

  const toggleTodo = (id: number) => {
    setTodos(todos =>
      todos.map(todo =>
        todo.id === id
          ? { ...todo, completed: !todo.completed }
          : todo
      )
    )
  }

  const removeTodo = (id: number) => {
    setTodos(todos => todos.filter(todo => todo.id !== id))
  }

  return (
    <ul>
      {/* For - efficient iteration with automatic keying */}
      <For each={todos()}>
        {(todo, index) => (
          <li>
            <input
              type="checkbox"
              checked={todo.completed}
              onChange={() => toggleTodo(todo.id)}
            />
            <span class={todo.completed ? 'completed' : ''}>
              {index() + 1}. {todo.text}
            </span>
            <button onClick={() => removeTodo(todo.id)}>×</button>
          </li>
        )}
      </For>
    </ul>
  )
}

Index - iteration with stable index

Code
TypeScript
import { Index, createSignal } from 'solid-js'

function IndexExample() {
  const [items, setItems] = createSignal(['A', 'B', 'C', 'D'])

  // Index - the index is stable, the value can change
  // For - the value is stable, the index can change

  return (
    <ul>
      <Index each={items()}>
        {(item, index) => (
          <li>
            {/* index is a number, item() is a signal */}
            {index}: {item()}
            <input
              value={item()}
              onInput={(e) => {
                const newItems = [...items()]
                newItems[index] = e.target.value
                setItems(newItems)
              }}
            />
          </li>
        )}
      </Index>
    </ul>
  )
}

Switch/Match - pattern matching

Code
TypeScript
import { Switch, Match, createSignal } from 'solid-js'

function StatusDisplay() {
  const [status, setStatus] = createSignal<'idle' | 'loading' | 'success' | 'error'>('idle')
  const [data, setData] = createSignal(null)
  const [error, setError] = createSignal(null)

  return (
    <div>
      <Switch fallback={<p>Unknown status</p>}>
        <Match when={status() === 'idle'}>
          <p>Click to load data</p>
        </Match>

        <Match when={status() === 'loading'}>
          <div class="spinner">Loading...</div>
        </Match>

        <Match when={status() === 'error'}>
          <div class="error">
            Error: {error()?.message}
            <button onClick={() => setStatus('idle')}>Retry</button>
          </div>
        </Match>

        <Match when={status() === 'success'}>
          <div class="success">
            <pre>{JSON.stringify(data(), null, 2)}</pre>
          </div>
        </Match>
      </Switch>
    </div>
  )
}

Dynamic - dynamic component

Code
TypeScript
import { Dynamic, createSignal } from 'solid-js'

// Components
const RedBox = () => <div style={{ background: 'red', padding: '20px' }}>Red</div>
const BlueBox = () => <div style={{ background: 'blue', padding: '20px' }}>Blue</div>
const GreenBox = () => <div style={{ background: 'green', padding: '20px' }}>Green</div>

function DynamicComponentExample() {
  const [selected, setSelected] = createSignal<'red' | 'blue' | 'green'>('red')

  const components = {
    red: RedBox,
    blue: BlueBox,
    green: GreenBox,
  }

  return (
    <div>
      <select onChange={(e) => setSelected(e.target.value as any)}>
        <option value="red">Red</option>
        <option value="blue">Blue</option>
        <option value="green">Green</option>
      </select>

      {/* Dynamic renders the selected component */}
      <Dynamic component={components[selected()]} />
    </div>
  )
}

Portal - rendering outside the tree

Code
TypeScript
import { Portal, createSignal } from 'solid-js'

function ModalExample() {
  const [isOpen, setIsOpen] = createSignal(false)

  return (
    <div>
      <button onClick={() => setIsOpen(true)}>Open Modal</button>

      <Show when={isOpen()}>
        {/* Portal renders children into document.body */}
        <Portal>
          <div class="modal-overlay" onClick={() => setIsOpen(false)}>
            <div class="modal" onClick={(e) => e.stopPropagation()}>
              <h2>Modal Title</h2>
              <p>Modal content here</p>
              <button onClick={() => setIsOpen(false)}>Close</button>
            </div>
          </div>
        </Portal>
      </Show>
    </div>
  )
}

Stores - state management

createStore - nested reactive state

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

interface User {
  id: number
  name: string
  email: string
  settings: {
    theme: 'light' | 'dark'
    notifications: boolean
  }
}

interface AppState {
  user: User | null
  todos: { id: number; text: string; completed: boolean }[]
  isLoading: boolean
}

function AppWithStore() {
  // createStore for complex objects
  const [state, setState] = createStore<AppState>({
    user: null,
    todos: [],
    isLoading: false,
  })

  // Updating a nested property - path syntax
  const setTheme = (theme: 'light' | 'dark') => {
    setState('user', 'settings', 'theme', theme)
  }

  // Updating with produce (immer-like)
  const addTodo = (text: string) => {
    setState(produce((s) => {
      s.todos.push({
        id: Date.now(),
        text,
        completed: false,
      })
    }))
  }

  const toggleTodo = (id: number) => {
    setState('todos', (todo) => todo.id === id, 'completed', (c) => !c)
  }

  // Login
  const login = async (email: string, password: string) => {
    setState('isLoading', true)

    try {
      const response = await fetch('/api/login', {
        method: 'POST',
        body: JSON.stringify({ email, password }),
      })
      const user = await response.json()
      setState('user', user)
    } finally {
      setState('isLoading', false)
    }
  }

  return (
    <div>
      <Show when={state.user} fallback={<LoginForm onLogin={login} />}>
        <p>Welcome, {state.user!.name}</p>

        <button onClick={() => setTheme(
          state.user!.settings.theme === 'light' ? 'dark' : 'light'
        )}>
          Toggle theme: {state.user!.settings.theme}
        </button>

        <For each={state.todos}>
          {(todo) => (
            <div>
              <input
                type="checkbox"
                checked={todo.completed}
                onChange={() => toggleTodo(todo.id)}
              />
              {todo.text}
            </div>
          )}
        </For>
      </Show>
    </div>
  )
}

reconcile - efficient updates

Code
TypeScript
import { createStore, reconcile } from 'solid-js/store'

function DataGrid() {
  const [data, setData] = createStore<{ items: Item[] }>({ items: [] })

  const fetchData = async () => {
    const response = await fetch('/api/items')
    const newItems = await response.json()

    // reconcile efficiently diffs and updates
    // instead of replacing the entire array
    setData('items', reconcile(newItems))
  }

  return (
    <div>
      <button onClick={fetchData}>Refresh</button>
      <For each={data.items}>
        {(item) => <ItemRow item={item} />}
      </For>
    </div>
  )
}

Context API

Code
TypeScript
import { createContext, useContext, ParentComponent } from 'solid-js'
import { createStore } from 'solid-js/store'

// Context type definition
interface AuthContextValue {
  user: () => User | null
  login: (email: string, password: string) => Promise<void>
  logout: () => void
  isLoading: () => boolean
}

// Creating context
const AuthContext = createContext<AuthContextValue>()

// Provider
export const AuthProvider: ParentComponent = (props) => {
  const [state, setState] = createStore({
    user: null as User | null,
    isLoading: false,
  })

  const login = async (email: string, password: string) => {
    setState('isLoading', true)
    try {
      const response = await fetch('/api/auth/login', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ email, password }),
      })
      const user = await response.json()
      setState('user', user)
    } finally {
      setState('isLoading', false)
    }
  }

  const logout = () => {
    setState('user', null)
  }

  const value: AuthContextValue = {
    user: () => state.user,
    login,
    logout,
    isLoading: () => state.isLoading,
  }

  return (
    <AuthContext.Provider value={value}>
      {props.children}
    </AuthContext.Provider>
  )
}

// Hook for using context
export function useAuth() {
  const context = useContext(AuthContext)
  if (!context) {
    throw new Error('useAuth must be used within AuthProvider')
  }
  return context
}

// Usage
function Profile() {
  const { user, logout } = useAuth()

  return (
    <Show when={user()}>
      <div>
        <p>Welcome, {user()!.name}</p>
        <button onClick={logout}>Logout</button>
      </div>
    </Show>
  )
}

// App
function App() {
  return (
    <AuthProvider>
      <Header />
      <Profile />
    </AuthProvider>
  )
}

Resources - async data

Code
TypeScript
import { createResource, Suspense, ErrorBoundary, Show } from 'solid-js'

// createResource for async data
function UserProfile(props: { userId: string }) {
  // First argument: source signal
  // Second argument: fetcher function
  const [user, { mutate, refetch }] = createResource(
    () => props.userId,
    async (userId) => {
      const response = await fetch(`/api/users/${userId}`)
      if (!response.ok) throw new Error('User not found')
      return response.json()
    }
  )

  return (
    <div>
      <Show when={user.loading}>
        <p>Loading...</p>
      </Show>

      <Show when={user.error}>
        <p>Error: {user.error.message}</p>
        <button onClick={refetch}>Retry</button>
      </Show>

      <Show when={user()}>
        <div>
          <h2>{user()!.name}</h2>
          <p>{user()!.email}</p>

          {/* Optimistic update */}
          <button onClick={() => {
            mutate({ ...user()!, name: 'Updated Name' })
            // Then sync with the server
          }}>
            Update Name
          </button>
        </div>
      </Show>
    </div>
  )
}

// With Suspense
function App() {
  const [userId, setUserId] = createSignal('1')

  return (
    <ErrorBoundary fallback={(err) => <p>Error: {err.message}</p>}>
      <Suspense fallback={<div>Loading user...</div>}>
        <UserProfile userId={userId()} />
      </Suspense>
    </ErrorBoundary>
  )
}

SolidStart - meta-framework

Routing

TSsrc/routes/index.tsx
TypeScript
// src/routes/index.tsx
export default function Home() {
  return (
    <main>
      <h1>Welcome to SolidStart</h1>
    </main>
  )
}
TSsrc/routes/blog/[slug].tsx
TypeScript
// src/routes/blog/[slug].tsx
import { useParams } from '@solidjs/router'
import { createAsync, cache } from '@solidjs/router'

// Cache for data fetching
const getPost = cache(async (slug: string) => {
  const response = await fetch(`/api/posts/${slug}`)
  return response.json()
}, 'post')

export default function BlogPost() {
  const params = useParams()

  // createAsync - suspense-enabled data fetching
  const post = createAsync(() => getPost(params.slug))

  return (
    <article>
      <h1>{post()?.title}</h1>
      <div innerHTML={post()?.content} />
    </article>
  )
}

Server functions

TSsrc/routes/api/users.ts
TypeScript
// src/routes/api/users.ts
import { json } from '@solidjs/router'

export async function GET() {
  const users = await db.users.findMany()
  return json(users)
}

export async function POST({ request }) {
  const body = await request.json()
  const user = await db.users.create({ data: body })
  return json(user, { status: 201 })
}
Code
TypeScript
// Server actions in components
import { action, useAction, useSubmission } from '@solidjs/router'

// Server action definition
const addTodo = action(async (formData: FormData) => {
  'use server'
  const text = formData.get('text') as string
  await db.todos.create({ data: { text } })
}, 'addTodo')

function TodoForm() {
  const submission = useSubmission(addTodo)

  return (
    <form action={addTodo} method="post">
      <input name="text" placeholder="New todo..." disabled={submission.pending} />
      <button type="submit" disabled={submission.pending}>
        {submission.pending ? 'Adding...' : 'Add'}
      </button>
    </form>
  )
}

Layouts

TSsrc/routes/(app).tsx
TypeScript
// src/routes/(app).tsx - Layout for route group
import { ParentComponent } from 'solid-js'

const AppLayout: ParentComponent = (props) => {
  return (
    <div class="app-layout">
      <Header />
      <nav>
        <A href="/dashboard">Dashboard</A>
        <A href="/settings">Settings</A>
      </nav>
      <main>
        {props.children}
      </main>
      <Footer />
    </div>
  )
}

export default AppLayout

Components - patterns

Props with defaults

Code
TypeScript
import { Component, mergeProps, splitProps } from 'solid-js'

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

const Button: Component<ButtonProps> = (props) => {
  // mergeProps for defaults
  const merged = mergeProps(
    { variant: 'primary', size: 'md', disabled: false },
    props
  )

  // splitProps separates props
  const [local, others] = splitProps(merged, [
    'variant', 'size', 'disabled', 'class', 'children'
  ])

  const classes = () =>
    `btn btn-${local.variant} btn-${local.size} ${local.class || ''}`

  return (
    <button
      class={classes()}
      disabled={local.disabled}
      {...others}
    >
      {local.children}
    </button>
  )
}

Ref forwarding

Code
TypeScript
import { Component, onMount } from 'solid-js'

interface InputProps {
  ref?: HTMLInputElement | ((el: HTMLInputElement) => void)
  placeholder?: string
}

const Input: Component<InputProps> = (props) => {
  let inputRef: HTMLInputElement

  onMount(() => {
    // Forward ref
    if (typeof props.ref === 'function') {
      props.ref(inputRef)
    }
  })

  return (
    <input
      ref={inputRef!}
      placeholder={props.placeholder}
      class="input"
    />
  )
}

// Usage
function Form() {
  let inputRef: HTMLInputElement

  onMount(() => {
    inputRef.focus()
  })

  return (
    <form>
      <Input ref={inputRef!} placeholder="Focus on mount" />
    </form>
  )
}

Children handling

Code
TypeScript
import { ParentComponent, children, JSX } from 'solid-js'

interface CardProps {
  title?: string
  children: JSX.Element
}

const Card: ParentComponent<CardProps> = (props) => {
  // children() helper for transforming children
  const resolved = children(() => props.children)

  return (
    <div class="card">
      <Show when={props.title}>
        <div class="card-header">{props.title}</div>
      </Show>
      <div class="card-body">
        {resolved()}
      </div>
    </div>
  )
}

Integrations

Tailwind CSS

Code
Bash
npm install -D tailwindcss postcss autoprefixer
npx tailwindcss init -p
JStailwind.config.js
JavaScript
// tailwind.config.js
module.exports = {
  content: ['./src/**/*.{js,jsx,ts,tsx}'],
  theme: { extend: {} },
  plugins: [],
}

Form handling (custom solution)

Code
TypeScript
import { createSignal, createEffect } from 'solid-js'
import { createStore } from 'solid-js/store'

// Simple form hook for Solid
function createForm<T extends Record<string, any>>(initialValues: T) {
  const [values, setValues] = createStore(initialValues)
  const [errors, setErrors] = createStore<Partial<Record<keyof T, string>>>({})
  const [touched, setTouched] = createStore<Partial<Record<keyof T, boolean>>>({})

  const setValue = (field: keyof T, value: any) => {
    setValues(field as any, value)
  }

  const setError = (field: keyof T, error: string) => {
    setErrors(field as any, error)
  }

  const setFieldTouched = (field: keyof T) => {
    setTouched(field as any, true)
  }

  const reset = () => {
    setValues(initialValues)
    setErrors({})
    setTouched({})
  }

  return {
    values,
    errors,
    touched,
    setValue,
    setError,
    setFieldTouched,
    reset,
  }
}

// Usage
function ContactForm() {
  const form = createForm({
    name: '',
    email: '',
    message: '',
  })

  const handleSubmit = async (e: Event) => {
    e.preventDefault()
    // Validation and submit
  }

  return (
    <form onSubmit={handleSubmit}>
      <input
        value={form.values.name}
        onInput={(e) => form.setValue('name', e.target.value)}
        onBlur={() => form.setFieldTouched('name')}
      />
      {form.touched.name && form.errors.name && (
        <span class="error">{form.errors.name}</span>
      )}
      {/* ... */}
    </form>
  )
}

Deployment

Vercel

Code
Bash
npm install @solidjs/start
TSapp.config.ts
TypeScript
// app.config.ts
import { defineConfig } from '@solidjs/start/config'

export default defineConfig({
  server: {
    preset: 'vercel',
  },
})

Cloudflare Pages

TSapp.config.ts
TypeScript
// app.config.ts
export default defineConfig({
  server: {
    preset: 'cloudflare-pages',
  },
})

Static export

TSapp.config.ts
TypeScript
// app.config.ts
export default defineConfig({
  server: {
    preset: 'static',
    prerender: {
      routes: ['/', '/about', '/blog'],
    },
  },
})

Performance tips

Code
TypeScript
// 1. Avoid unnecessary wrappers
// ❌
<div>{count()}</div>

// ✅
<span>{count()}</span>

// 2. Use createMemo for expensive computations
const expensive = createMemo(() => {
  return items().filter(/* ... */).map(/* ... */)
})

// 3. Batch updates
import { batch } from 'solid-js'

batch(() => {
  setCount(count() + 1)
  setName('New name')
  setItems([...items(), newItem])
})

// 4. untrack for reading without creating a dependency
import { untrack } from 'solid-js'

createEffect(() => {
  const currentCount = count() // tracked
  const staticValue = untrack(() => someSignal()) // not tracked
})

// 5. Lazy components
import { lazy } from 'solid-js'

const HeavyComponent = lazy(() => import('./HeavyComponent'))

Pricing

  • 100% free - MIT License

FAQ - frequently asked questions

Can I use React libraries with SolidJS?

Not directly. SolidJS has a different reactivity model. However, many libraries have Solid equivalents or you can write a wrapper. The community is actively creating solid-* versions of popular libraries.

Why count() instead of count?

Signals in SolidJS are functions. Calling count() reads the value and registers a dependency. This enables fine-grained tracking without a Virtual DOM.

Is SolidJS suitable for large applications?

Yes. SolidJS scales excellently thanks to fine-grained reactivity. It does not suffer from the problem of re-rendering large component trees like React does.

What is the learning curve like?

For React developers it is very low. The JSX syntax is nearly identical. The main differences are using count() instead of count and control flow components (Show, For).

Does SolidJS have an ecosystem?

A growing one. There is SolidStart (meta-framework), solid-ui, solid-primitives (utility hooks), and many others. The community is smaller than React's, but very active.