Zum Inhalt springen

SolidJS

Feingranulares reaktives JavaScript-UI-Framework ohne virtuelles DOM und mit außergewöhnlicher Leistung.

BefehlBeschreibung
npx degit solidjs/templates/ts my-appTypeScript-SolidJS-Projekt erstellen
npx degit solidjs/templates/js my-appJavaScript-SolidJS-Projekt erstellen
npm create solid@latestProjekt mit SolidStart-CLI erstellen
npm install solid-jsSolidJS in bestehendem Projekt installieren
npm install solid-startSolidStart-Meta-Framework installieren
BefehlBeschreibung
npm run devEntwicklungsserver mit Hot Reload starten
npm run buildFür Produktion bauen
npm run serveProduktions-Build lokal anzeigen
npm run startSolidStart-Server starten
# Einfaches 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
# Vorlage wählen: bare, with-auth, with-mdx, with-prisma, with-tailwindcss

# Community-Vorlagen
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
BefehlBeschreibung
function App() { return <div>Hello</div> }Grundlegende Komponente definieren
<MyComponent name="value" />Props an eine Komponente übergeben
props.childrenAuf Kinderinhalte zugreifen
<div class={styles.container}>CSS-Klasse anwenden (verwende class, nicht className)
<div classList={{ active: isActive() }}>Bedingte CSS-Klassen
<div style={{ color: 'red', 'font-size': '14px' }}>Inline-Styles anwenden
<div innerHTML={htmlString} />Roh-HTML-Inhalt setzen
<div ref={myRef}>DOM-Referenz anhängen
BefehlBeschreibung
<div onClick={handler}>Delegiertes Event anhängen
<div on:click={handler}>Natives DOM-Event anhängen (umgeht Delegation)
<div onInput={(e) => setValue(e.target.value)}>Eingabe-Events verarbeiten
<div on:keydown={(e) => handleKey(e)}>Tastatur-Events nativ verarbeiten
<div onClick={[handler, data]}>Daten mit Event-Handler übergeben
import { type Component, type ParentComponent } from 'solid-js'

// Typisierte Komponente mit 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>
  )
}

// Eltern-Komponente (akzeptiert Kinder)
const Layout: ParentComponent = (props) => {
  return (
    <div class="layout">
      <header>My App</header>
      <main>{props.children}</main>
      <footer>Footer</footer>
    </div>
  )
}

// Props aufteilen zum Weiterleiten
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}
    />
  )
}

// Standard-Props zusammenführen
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>
}
BefehlBeschreibung
const [count, setCount] = createSignal(0)Reaktives Signal erstellen
count()Signalwert lesen (muss als Funktion aufgerufen werden)
setCount(5)Signal auf bestimmten Wert setzen
setCount(prev => prev + 1)Signal mit vorherigem Wert aktualisieren
const [name, setName] = createSignal<string>()Typisiertes Signal erstellen
BefehlBeschreibung
createEffect(() => console.log(count()))Seiteneffekt bei Signaländerung ausführen
createEffect(on(count, (v) => log(v)))Bestimmtes Signal explizit verfolgen
createEffect(on([a, b], ([a, b]) => ...))Mehrere Signale explizit verfolgen
const double = createMemo(() => count() * 2)Abgeleiteten/berechneten Wert erstellen
createRenderEffect(() => ...)Effekt der vor dem DOM-Paint läuft
createComputed(() => ...)Synchroner Effekt während des Renderns
BefehlBeschreibung
onMount(() => { ... })Einmal ausführen wenn Komponente gemountet wird
onCleanup(() => { ... })Aufräumen wenn Komponente unmountet wird
batch(() => { setA(1); setB(2) })Mehrere Signal-Updates bündeln
untrack(() => value())Signal lesen ohne Abhängigkeit zu verfolgen
import { createSignal, createEffect, createMemo, on, batch, untrack } from 'solid-js'

function Counter() {
  const [count, setCount] = createSignal(0)
  const [step, setStep] = createSignal(1)

  // Abgeleiteter Wert (memoized, berechnet nur wenn count sich ändert)
  const doubled = createMemo(() => count() * 2)
  const isEven = createMemo(() => count() % 2 === 0)

  // Effekt: läuft wenn sich ein zugegriffenes Signal ändert
  createEffect(() => {
    console.log(`Count is now: ${count()}`)
    // Verfolgt automatisch count() als Abhängigkeit
  })

  // Explizites Tracking mit on()
  createEffect(on(count, (value, prev) => {
    console.log(`Changed from ${prev} to ${value}`)
  }, { defer: true })) // defer: initialen Lauf überspringen

  // Updates bündeln (einmaliges Re-Render)
  const reset = () => batch(() => {
    setCount(0)
    setStep(1)
  })

  // Lesen ohne Abhängigkeit zu erstellen
  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>
  )
}
BefehlBeschreibung
<Show when={loggedIn()} fallback={<Login/>}>Bedingtes Rendern
<For each={items()}>{(item) => <li>{item}</li>}</For>Liste rendern (nach Referenz geschlüsselt)
<Index each={items()}>{(item, i) => <li>{item()}</li>}</Index>Liste rendern (nach Index geschlüsselt)
<Switch><Match when={a()}>A</Match></Switch>Switch/Case-Rendering
<ErrorBoundary fallback={err => <p>{err}</p>}>Render-Fehler abfangen
<Suspense fallback={<Loading/>}>Fallback während asynchronem Laden anzeigen
<Dynamic component={MyComp} />Dynamische Komponente rendern
<Portal mount={document.body}>In anderen DOM-Knoten rendern
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: bedingtes Rendern mit Fallback */}
      <Show when={user()} fallback={<p>Please log in</p>}>
        {(u) => <p>Welcome, {u().name}!</p>}
      </Show>

      {/* For: nach Referenz geschlüsselt (am besten für Objekte) */}
      <For each={items()}>
        {(item, index) => (
          <div>
            <span>{index() + 1}. {item.name}</span>
            <span> ({item.status})</span>
          </div>
        )}
      </For>

      {/* Switch/Match: mehrere Bedingungen */}
      <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: Fehler im Kindbaum abfangen */}
      <ErrorBoundary fallback={(err, reset) => (
        <div>
          <p>Error: {err.message}</p>
          <button onClick={reset}>Try Again</button>
        </div>
      )}>
        <RiskyComponent />
      </ErrorBoundary>
    </div>
  )
}
BefehlBeschreibung
const [store, setStore] = createStore({})Reaktiven Store erstellen
setStore('name', 'Alice')Store-Eigenschaft nach Pfad aktualisieren
setStore('users', 0, 'name', 'Bob')Verschachtelte Store-Eigenschaft aktualisieren
setStore('list', l => [...l, item])Element zum Store-Array hinzufügen
setStore('list', i => i.id === 1, 'done', true)Übereinstimmende Array-Elemente aktualisieren
produce(s => { s.count++ })Mutationsartige Store-Updates
reconcile(newData)Store-Daten effizient ersetzen
unwrap(store)Rohe nicht-proxied Daten abrufen
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' } },
  })

  // Element hinzufügen
  const addTodo = (text: string) => {
    setState('todos', todos => [
      ...todos,
      { id: Date.now(), text, completed: false }
    ])
  }

  // Bestimmtes Element durch Abgleich umschalten
  const toggleTodo = (id: number) => {
    setState('todos', todo => todo.id === id, 'completed', c => !c)
  }

  // Element löschen (Filter erzeugt neues Array)
  const deleteTodo = (id: number) => {
    setState('todos', todos => todos.filter(t => t.id !== id))
  }

  // Mutationsartige Updates mit produce
  const clearCompleted = () => {
    setState(produce((s) => {
      s.todos = s.todos.filter(t => !t.completed)
    }))
  }

  // Tief verschachteltes Update
  const setTheme = (theme: string) => {
    setState('user', 'settings', 'theme', theme)
  }

  // Gesamte Store-Daten ersetzen (reconcile differenziert effizient)
  const loadFromServer = async () => {
    const data = await fetch('/api/todos').then(r => r.json())
    setState('todos', reconcile(data))
  }

  return (/* ... */)
}
BefehlBeschreibung
const [data] = createResource(fetchFn)Asynchrone Resource erstellen
const [data] = createResource(id, fetchFn)Resource mit reaktivem Quellsignal
data()Auf Resource-Daten zugreifen
data.loadingPrüfen ob Resource lädt
data.errorAuf Resource-Fehler zugreifen
data.latestLetzten aufgelösten Wert abrufen (überlebt Refetch)
data.stateResource-Zustand abrufen (‘unresolved’, ‘pending’, ‘ready’, ‘errored’)
const { refetch } = dataManuell Refetch auslösen
createResource(source, fetcher, { initialValue })Resource mit Anfangswert
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 aktualisiert automatisch wenn userId() sich ändert
  const [user, { refetch, mutate }] = createResource(userId, fetchUser)

  // Optimistisches Update
  const updateName = async (newName: string) => {
    const prev = user()
    mutate({ ...prev!, name: newName }) // optimistisch
    try {
      await fetch(`/api/users/${userId()}`, {
        method: 'PATCH',
        body: JSON.stringify({ name: newName }),
      })
    } catch {
      mutate(prev) // Rollback bei Fehler
    }
  }

  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>
  )
}
BefehlBeschreibung
npm install @solidjs/routerSolidJS-Router installieren
<Router><Route path="/" component={Home}/></Router>Grundlegende Route definieren
<Route path="/users/:id" component={User}/>Route mit Parameter
<Route path="/*all" component={NotFound}/>Catch-All-Route
const params = useParams()Auf Routenparameter zugreifen
const [searchParams, setSearchParams] = useSearchParams()Auf Query-Parameter zugreifen
const navigate = useNavigate()Programmatische Navigation
navigate('/dashboard')Zu Pfad navigieren
<A href="/about">About</A>Navigationslink-Komponente
<A href="/about" activeClass="active">Link mit aktivem Styling
import { Router, Route, A, useParams, useNavigate, useSearchParams } from '@solidjs/router'
import { lazy } from 'solid-js'

// Lazy-geladene Routen
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>
  )
}

// Verschachtelte Routen
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
}

// Verwendung
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>
  )
}
BefehlBeschreibung
import styles from './App.module.css'CSS-Module importieren
<div class={styles.container}>CSS-Modul-Klasse anwenden
npm install solid-styled-componentsStyled-Components für Solid installieren
const Btn = styled('button')\color: red“Gestylte Komponente erstellen
<div class="static-class">Statische CSS-Klasse anwenden
# Tailwind CSS installieren
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;
BefehlBeschreibung
npm create solid@latestSolidStart-Projekt erstellen
"use server" DirektiveFunktion als Server-exklusiv markieren
createServerAction$()Server-Action erstellen
createRouteData()Daten für eine Route auf dem Server laden
<Title>Page Title</Title>Seitentitel setzen (von @solidjs/meta)
<Meta name="description" content="..." />Meta-Tags setzen
// src/routes/todos.tsx
import { createAsync, query, action, redirect } from '@solidjs/router'

// Server-Query (Daten laden)
const getTodos = query(async () => {
  'use server'
  const db = await getDatabase()
  return db.todos.findMany()
}, 'todos')

// Server-Action (Mutationen)
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') // revalidiert
})

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. Signale immer als Funktionen aufrufencount() nicht count. Das Vergessen der Klammern ist der häufigste SolidJS-Fehler; ohne sie übergeben Sie die Getter-Funktion statt des Werts.

  2. Props nicht destrukturieren — Destrukturierung bricht die Reaktivität, da sie den Wert nur einmal liest. props.name direkt verwenden oder splitProps()/mergeProps() für Prop-Manipulation nutzen.

  3. <For> für Objekt-Arrays verwenden, <Index> für Primitive<For> schlüsselt nach Referenz (besser für Objekte die umgeordnet werden können) während <Index> nach Position schlüsselt (besser für einfache Wertelisten).

  4. Stores für komplexen Zustand bevorzugen — Signale sind großartig für einfache Werte, aber für verschachtelte Objekte und Arrays bietet createStore granulare Reaktivität ohne das gesamte Objekt zu ersetzen.

  5. on() für explizites Tracking verwenden — wenn ein Effekt nur bestimmte Signale verfolgen soll (nicht jedes im Inneren gelesene Signal), mit on(signal, callback) umschließen.

  6. Suspense und ErrorBoundary nutzen — Asynchrone Komponenten in Suspense für Ladezustände und ErrorBoundary für elegante Fehlerbehandlung umschließen.

  7. Routen lazy ladenlazy(() => import('./Page')) für Routen-Komponenten verwenden, um Code-Splitting zu ermöglichen und die initiale Bundle-Größe zu reduzieren.

  8. batch() für mehrere Updates verwenden — Wenn mehrere Signale gleichzeitig gesetzt werden, in batch() umschließen, um ein einzelnes Re-Render statt mehrerer auszulösen.

  9. Komponenten klein halten — SolidJS-Komponenten führen ihren Body nur einmal aus (nicht bei jedem Render wie React), daher große Komponenten aufteilen, um den Umfang reaktiver Updates zu begrenzen.

  10. TypeScript-Generics mit Signalen verwendencreateSignal<string | null>(null) bietet bessere Typsicherheit und Autovervollständigung für den reaktiven Zustand.

  11. Das Kompilierungsmodell verstehen — SolidJS kompiliert JSX zur Build-Zeit zu echten DOM-Operationen. Es gibt kein virtuelles DOM-Diffing, weshalb es schnell ist, aber auch warum die Reaktivitätsregeln (kein Destrukturieren, Signale aufrufen) wichtig sind.

  12. untrack() sparsam verwenden — Das Lesen eines Signals innerhalb von untrack() verhindert Abhängigkeitsverfolgung. Nützlich für Logging oder einmalige Lesevorgänge, kann aber bei übermäßiger Nutzung Fehler verursachen.