Salta ai contenuti

SolidJS

Framework UI JavaScript reattivo a grana fine senza virtual DOM e con prestazioni eccezionali.

ComandoDescrizione
npx degit solidjs/templates/ts my-appCrea un progetto SolidJS TypeScript
npx degit solidjs/templates/js my-appCrea un progetto SolidJS JavaScript
npm create solid@latestCrea un progetto con la CLI di SolidStart
npm install solid-jsInstalla SolidJS in un progetto esistente
npm install solid-startInstalla il meta-framework SolidStart
ComandoDescrizione
npm run devAvvia il server di sviluppo con hot reload
npm run buildBuild per produzione
npm run serveAnteprima della build di produzione in locale
npm run startAvvia il server SolidStart
# SolidJS base (Vite)
npx degit solidjs/templates/ts my-app
cd my-app && npm install

# SolidStart (meta-framework full-stack)
npm create solid@latest my-app
# Scegli il template: bare, with-auth, with-mdx, with-prisma, with-tailwindcss

# Template della community
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
ComandoDescrizione
function App() { return <div>Hello</div> }Definisci un componente base
<MyComponent name="value" />Passa props a un componente
props.childrenAccedi al contenuto figlio
<div class={styles.container}>Applica classe CSS (usa class, non className)
<div classList={{ active: isActive() }}>Classi CSS condizionali
<div style={{ color: 'red', 'font-size': '14px' }}>Applica stili inline
<div innerHTML={htmlString} />Imposta contenuto HTML grezzo
<div ref={myRef}>Collega riferimento DOM
ComandoDescrizione
<div onClick={handler}>Collega evento delegato
<div on:click={handler}>Collega evento DOM nativo (bypassa la delega)
<div onInput={(e) => setValue(e.target.value)}>Gestisci eventi di input
<div on:keydown={(e) => handleKey(e)}>Gestisci eventi tastiera nativamente
<div onClick={[handler, data]}>Passa dati con il gestore eventi
import { type Component, type ParentComponent } from 'solid-js'

// Componente tipizzato con 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>
  )
}

// Componente padre (accetta figli)
const Layout: ParentComponent = (props) => {
  return (
    <div class="layout">
      <header>My App</header>
      <main>{props.children}</main>
      <footer>Footer</footer>
    </div>
  )
}

// Separazione delle props per l'inoltro
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}
    />
  )
}

// Unione di props predefinite
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>
}
ComandoDescrizione
const [count, setCount] = createSignal(0)Crea un signal reattivo
count()Leggi il valore del signal (devi chiamarlo come funzione)
setCount(5)Imposta il signal a un valore specifico
setCount(prev => prev + 1)Aggiorna il signal con il valore precedente
const [name, setName] = createSignal<string>()Crea un signal tipizzato
ComandoDescrizione
createEffect(() => console.log(count()))Esegui effetto collaterale al cambio del signal
createEffect(on(count, (v) => log(v)))Traccia esplicitamente un signal specifico
createEffect(on([a, b], ([a, b]) => ...))Traccia esplicitamente signal multipli
const double = createMemo(() => count() * 2)Crea valore derivato/calcolato
createRenderEffect(() => ...)Effetto che viene eseguito prima del paint del DOM
createComputed(() => ...)Effetto sincrono durante il rendering
ComandoDescrizione
onMount(() => { ... })Esegui una volta quando il componente viene montato
onCleanup(() => { ... })Esegui pulizia quando il componente viene smontato
batch(() => { setA(1); setB(2) })Raggruppa aggiornamenti multipli dei signal
untrack(() => value())Leggi il signal senza tracciare la dipendenza
import { createSignal, createEffect, createMemo, on, batch, untrack } from 'solid-js'

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

  // Valore derivato (memoizzato, ricalcola solo quando count cambia)
  const doubled = createMemo(() => count() * 2)
  const isEven = createMemo(() => count() % 2 === 0)

  // Effect: viene eseguito quando qualsiasi signal acceduto cambia
  createEffect(() => {
    console.log(`Count is now: ${count()}`)
    // Traccia automaticamente count() come dipendenza
  })

  // Tracciamento esplicito con on()
  createEffect(on(count, (value, prev) => {
    console.log(`Changed from ${prev} to ${value}`)
  }, { defer: true })) // defer: salta l'esecuzione iniziale

  // Aggiornamenti raggruppati (singolo re-render)
  const reset = () => batch(() => {
    setCount(0)
    setStep(1)
  })

  // Lettura senza creare dipendenza
  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>
  )
}
ComandoDescrizione
<Show when={loggedIn()} fallback={<Login/>}>Rendering condizionale
<For each={items()}>{(item) => <li>{item}</li>}</For>Rendering di lista (chiave per riferimento)
<Index each={items()}>{(item, i) => <li>{item()}</li>}</Index>Rendering di lista (chiave per indice)
<Switch><Match when={a()}>A</Match></Switch>Rendering switch/case
<ErrorBoundary fallback={err => <p>{err}</p>}>Cattura errori di rendering
<Suspense fallback={<Loading/>}>Mostra fallback durante il caricamento asincrono
<Dynamic component={MyComp} />Rendering di componente dinamico
<Portal mount={document.body}>Rendering in un nodo DOM diverso
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: rendering condizionale con fallback */}
      <Show when={user()} fallback={<p>Please log in</p>}>
        {(u) => <p>Welcome, {u().name}!</p>}
      </Show>

      {/* For: chiave per riferimento (migliore per oggetti) */}
      <For each={items()}>
        {(item, index) => (
          <div>
            <span>{index() + 1}. {item.name}</span>
            <span> ({item.status})</span>
          </div>
        )}
      </For>

      {/* Switch/Match: condizioni multiple */}
      <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: cattura errori nell'albero figlio */}
      <ErrorBoundary fallback={(err, reset) => (
        <div>
          <p>Error: {err.message}</p>
          <button onClick={reset}>Try Again</button>
        </div>
      )}>
        <RiskyComponent />
      </ErrorBoundary>
    </div>
  )
}
ComandoDescrizione
const [store, setStore] = createStore({})Crea uno store reattivo
setStore('name', 'Alice')Aggiorna proprieta dello store per percorso
setStore('users', 0, 'name', 'Bob')Aggiorna proprieta annidata dello store
setStore('list', l => [...l, item])Aggiungi elemento all’array dello store
setStore('list', i => i.id === 1, 'done', true)Aggiorna elementi dell’array che corrispondono
produce(s => { s.count++ })Aggiornamenti dello store in stile mutabile
reconcile(newData)Sostituisci i dati dello store in modo efficiente
unwrap(store)Ottieni i dati grezzi non-proxy
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' } },
  })

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

  // Attiva/disattiva elemento specifico per corrispondenza
  const toggleTodo = (id: number) => {
    setState('todos', todo => todo.id === id, 'completed', c => !c)
  }

  // Elimina elemento (filter produce un nuovo array)
  const deleteTodo = (id: number) => {
    setState('todos', todos => todos.filter(t => t.id !== id))
  }

  // Aggiornamenti in stile mutabile con produce
  const clearCompleted = () => {
    setState(produce((s) => {
      s.todos = s.todos.filter(t => !t.completed)
    }))
  }

  // Aggiornamento annidato profondo
  const setTheme = (theme: string) => {
    setState('user', 'settings', 'theme', theme)
  }

  // Sostituisci tutti i dati dello store (reconcile calcola le differenze in modo efficiente)
  const loadFromServer = async () => {
    const data = await fetch('/api/todos').then(r => r.json())
    setState('todos', reconcile(data))
  }

  return (/* ... */)
}
ComandoDescrizione
const [data] = createResource(fetchFn)Crea una resource asincrona
const [data] = createResource(id, fetchFn)Resource con signal sorgente reattivo
data()Accedi ai dati della resource
data.loadingControlla se la resource e in caricamento
data.errorAccedi all’errore della resource
data.latestOttieni l’ultimo valore risolto (sopravvive al refetch)
data.stateOttieni lo stato della resource (‘unresolved’, ‘pending’, ‘ready’, ‘errored’)
const { refetch } = dataAttiva manualmente il refetch
createResource(source, fetcher, { initialValue })Resource con valore iniziale
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)

  // La resource si aggiorna automaticamente quando userId() cambia
  const [user, { refetch, mutate }] = createResource(userId, fetchUser)

  // Aggiornamento ottimistico
  const updateName = async (newName: string) => {
    const prev = user()
    mutate({ ...prev!, name: newName }) // ottimistico
    try {
      await fetch(`/api/users/${userId()}`, {
        method: 'PATCH',
        body: JSON.stringify({ name: newName }),
      })
    } catch {
      mutate(prev) // rollback in caso di errore
    }
  }

  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>
  )
}
ComandoDescrizione
npm install @solidjs/routerInstalla il router di SolidJS
<Router><Route path="/" component={Home}/></Router>Definisci una route base
<Route path="/users/:id" component={User}/>Route con parametro
<Route path="/*all" component={NotFound}/>Route catch-all
const params = useParams()Accedi ai parametri della route
const [searchParams, setSearchParams] = useSearchParams()Accedi ai parametri di query
const navigate = useNavigate()Navigazione programmatica
navigate('/dashboard')Naviga verso un percorso
<A href="/about">About</A>Componente link di navigazione
<A href="/about" activeClass="active">Link con stile attivo
import { Router, Route, A, useParams, useNavigate, useSearchParams } from '@solidjs/router'
import { lazy } from 'solid-js'

// Route caricate in modo lazy
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>
  )
}

// Route annidate
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
}

// Utilizzo
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>
  )
}
ComandoDescrizione
import styles from './App.module.css'Importa moduli CSS
<div class={styles.container}>Applica classe da modulo CSS
npm install solid-styled-componentsInstalla styled-components per Solid
const Btn = styled('button')\color: red“Crea componente stilizzato
<div class="static-class">Applica classe CSS statica
# Installa 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;
ComandoDescrizione
npm create solid@latestCrea un progetto SolidStart
"use server" directiveSegna una funzione come solo server
createServerAction$()Crea un’azione server
createRouteData()Carica dati per una route sul server
<Title>Page Title</Title>Imposta il titolo della pagina (da @solidjs/meta)
<Meta name="description" content="..." />Imposta meta tag
// src/routes/todos.tsx
import { createAsync, query, action, redirect } from '@solidjs/router'

// Query server (caricamento dati)
const getTodos = query(async () => {
  'use server'
  const db = await getDatabase()
  return db.todos.findMany()
}, 'todos')

// Azione server (mutazioni)
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') // rivalidazione
})

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. Chiama sempre i signal come funzionicount() non count. Dimenticare le parentesi e l’errore piu comune in SolidJS; senza di esse passi la funzione getter invece del valore.

  2. Non destrutturare le props — la destrutturazione interrompe la reattivita perche legge il valore una sola volta. Usa props.name direttamente o usa splitProps()/mergeProps() per la manipolazione delle props.

  3. Usa <For> per array di oggetti, <Index> per primitive<For> usa come chiave il riferimento (migliore per oggetti che possono essere riordinati) mentre <Index> usa come chiave la posizione (migliore per liste di valori semplici).

  4. Preferisci gli store per lo stato complesso — i signal sono ottimi per valori semplici, ma per oggetti annidati e array, createStore fornisce reattivita granulare senza sostituire l’intero oggetto.

  5. Usa on() per il tracciamento esplicito — quando vuoi che un effect tracci solo signal specifici (non ogni signal letto al suo interno), avvolgilo con on(signal, callback).

  6. Sfrutta Suspense e ErrorBoundary — avvolgi i componenti asincroni in Suspense per gli stati di caricamento e ErrorBoundary per la gestione elegante degli errori.

  7. Carica le route in modo lazy — usa lazy(() => import('./Page')) per i componenti delle route per abilitare il code splitting e ridurre la dimensione del bundle iniziale.

  8. Usa batch() per aggiornamenti multipli — quando imposti diversi signal contemporaneamente, avvolgili in batch() per attivare un singolo re-render invece di multipli.

  9. Mantieni i componenti piccoli — i componenti SolidJS eseguono il loro corpo solo una volta (non ad ogni render come React), quindi dividi i componenti grandi per limitare la portata degli aggiornamenti reattivi.

  10. Usa i generics TypeScript con i signalcreateSignal<string | null>(null) fornisce migliore sicurezza dei tipi e autocompletamento per il tuo stato reattivo.

  11. Comprendi il modello di compilazione — SolidJS compila il JSX in operazioni DOM reali al momento del build. Non c’e diffing del virtual DOM, ed e per questo che e veloce ma anche perche le regole di reattivita (niente destrutturazione, chiama i signal) sono importanti.

  12. Usa untrack() con parsimonia — leggere un signal dentro untrack() impedisce il tracciamento delle dipendenze. Questo e utile per il logging o letture una tantum ma puo causare bug se usato eccessivamente.