Aller au contenu

SolidJS

Framework UI JavaScript réactif à grain fin sans DOM virtuel avec des performances exceptionnelles.

CommandeDescription
npx degit solidjs/templates/ts my-appCréer un projet SolidJS TypeScript
npx degit solidjs/templates/js my-appCréer un projet SolidJS JavaScript
npm create solid@latestCréer un projet avec le CLI SolidStart
npm install solid-jsInstaller SolidJS dans un projet existant
npm install solid-startInstaller le méta-framework SolidStart
CommandeDescription
npm run devDémarrer le serveur de développement avec hot reload
npm run buildCompiler pour la production
npm run servePrévisualiser la compilation de production localement
npm run startDémarrer le serveur SolidStart
# SolidJS basique (Vite)
npx degit solidjs/templates/ts my-app
cd my-app && npm install

# SolidStart (méta-framework full-stack)
npm create solid@latest my-app
# Choisir le modèle : bare, with-auth, with-mdx, with-prisma, with-tailwindcss

# Modèles communautaires
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
CommandeDescription
function App() { return <div>Hello</div> }Définir un composant basique
<MyComponent name="value" />Passer des props à un composant
props.childrenAccéder au contenu enfant
<div class={styles.container}>Appliquer une classe CSS (utiliser class, pas className)
<div classList={{ active: isActive() }}>Classes CSS conditionnelles
<div style={{ color: 'red', 'font-size': '14px' }}>Appliquer des styles en ligne
<div innerHTML={htmlString} />Définir du contenu HTML brut
<div ref={myRef}>Attacher une référence DOM
CommandeDescription
<div onClick={handler}>Attacher un événement délégué
<div on:click={handler}>Attacher un événement DOM natif (contourne la délégation)
<div onInput={(e) => setValue(e.target.value)}>Gérer les événements de saisie
<div on:keydown={(e) => handleKey(e)}>Gérer les événements clavier nativement
<div onClick={[handler, data]}>Passer des données avec le gestionnaire d’événements
import { type Component, type ParentComponent } from 'solid-js'

// Composant typé avec 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>
  )
}

// Composant parent (accepte des enfants)
const Layout: ParentComponent = (props) => {
  return (
    <div class="layout">
      <header>My App</header>
      <main>{props.children}</main>
      <footer>Footer</footer>
    </div>
  )
}

// Séparer les props pour le transfert
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}
    />
  )
}

// Fusionner les props par défaut
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>
}
CommandeDescription
const [count, setCount] = createSignal(0)Créer un signal réactif
count()Lire la valeur du signal (doit être appelé comme une fonction)
setCount(5)Définir le signal à une valeur spécifique
setCount(prev => prev + 1)Mettre à jour le signal avec la valeur précédente
const [name, setName] = createSignal<string>()Créer un signal typé
CommandeDescription
createEffect(() => console.log(count()))Exécuter un effet de bord lors du changement de signal
createEffect(on(count, (v) => log(v)))Suivre explicitement un signal spécifique
createEffect(on([a, b], ([a, b]) => ...))Suivre explicitement plusieurs signaux
const double = createMemo(() => count() * 2)Créer une valeur dérivée/calculée
createRenderEffect(() => ...)Effet qui s’exécute avant le rendu DOM
createComputed(() => ...)Effet synchrone pendant le rendu
CommandeDescription
onMount(() => { ... })Exécuter une fois quand le composant est monté
onCleanup(() => { ... })Exécuter le nettoyage quand le composant est démonté
batch(() => { setA(1); setB(2) })Grouper les mises à jour de plusieurs signaux
untrack(() => value())Lire un signal sans suivre la dépendance
import { createSignal, createEffect, createMemo, on, batch, untrack } from 'solid-js'

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

  // Valeur dérivée (mémorisée, recalculée seulement quand count change)
  const doubled = createMemo(() => count() * 2)
  const isEven = createMemo(() => count() % 2 === 0)

  // Effet : s'exécute quand un signal accédé change
  createEffect(() => {
    console.log(`Count is now: ${count()}`)
    // Suit automatiquement count() comme dépendance
  })

  // Suivi explicite avec on()
  createEffect(on(count, (value, prev) => {
    console.log(`Changed from ${prev} to ${value}`)
  }, { defer: true })) // defer : ignorer l'exécution initiale

  // Grouper les mises à jour (un seul re-rendu)
  const reset = () => batch(() => {
    setCount(0)
    setStep(1)
  })

  // Lire sans créer de dépendance
  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>
  )
}
CommandeDescription
<Show when={loggedIn()} fallback={<Login/>}>Rendu conditionnel
<For each={items()}>{(item) => <li>{item}</li>}</For>Rendu de liste (clé par référence)
<Index each={items()}>{(item, i) => <li>{item()}</li>}</Index>Rendu de liste (clé par index)
<Switch><Match when={a()}>A</Match></Switch>Rendu switch/case
<ErrorBoundary fallback={err => <p>{err}</p>}>Capturer les erreurs de rendu
<Suspense fallback={<Loading/>}>Afficher un fallback pendant le chargement asynchrone
<Dynamic component={MyComp} />Rendre un composant dynamique
<Portal mount={document.body}>Rendre dans un autre nœud DOM
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 : rendu conditionnel avec fallback */}
      <Show when={user()} fallback={<p>Please log in</p>}>
        {(u) => <p>Welcome, {u().name}!</p>}
      </Show>

      {/* For : clé par référence (meilleur pour les objets) */}
      <For each={items()}>
        {(item, index) => (
          <div>
            <span>{index() + 1}. {item.name}</span>
            <span> ({item.status})</span>
          </div>
        )}
      </For>

      {/* Switch/Match : conditions multiples */}
      <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 : capturer les erreurs dans l'arbre enfant */}
      <ErrorBoundary fallback={(err, reset) => (
        <div>
          <p>Error: {err.message}</p>
          <button onClick={reset}>Try Again</button>
        </div>
      )}>
        <RiskyComponent />
      </ErrorBoundary>
    </div>
  )
}
CommandeDescription
const [store, setStore] = createStore({})Créer un store réactif
setStore('name', 'Alice')Mettre à jour une propriété du store par chemin
setStore('users', 0, 'name', 'Bob')Mettre à jour une propriété imbriquée du store
setStore('list', l => [...l, item])Ajouter un élément au tableau du store
setStore('list', i => i.id === 1, 'done', true)Mettre à jour les éléments correspondants du tableau
produce(s => { s.count++ })Mises à jour du store de style mutable
reconcile(newData)Remplacer les données du store efficacement
unwrap(store)Obtenir les données brutes non-proxifiées
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' } },
  })

  // Ajouter un élément
  const addTodo = (text: string) => {
    setState('todos', todos => [
      ...todos,
      { id: Date.now(), text, completed: false }
    ])
  }

  // Basculer un élément spécifique par correspondance
  const toggleTodo = (id: number) => {
    setState('todos', todo => todo.id === id, 'completed', c => !c)
  }

  // Supprimer un élément (filter produit un nouveau tableau)
  const deleteTodo = (id: number) => {
    setState('todos', todos => todos.filter(t => t.id !== id))
  }

  // Mises à jour de style mutable avec produce
  const clearCompleted = () => {
    setState(produce((s) => {
      s.todos = s.todos.filter(t => !t.completed)
    }))
  }

  // Mise à jour imbriquée profonde
  const setTheme = (theme: string) => {
    setState('user', 'settings', 'theme', theme)
  }

  // Remplacer toutes les données du store (reconcile effectue un diff efficace)
  const loadFromServer = async () => {
    const data = await fetch('/api/todos').then(r => r.json())
    setState('todos', reconcile(data))
  }

  return (/* ... */)
}
CommandeDescription
const [data] = createResource(fetchFn)Créer une ressource asynchrone
const [data] = createResource(id, fetchFn)Ressource avec signal source réactif
data()Accéder aux données de la ressource
data.loadingVérifier si la ressource est en cours de chargement
data.errorAccéder à l’erreur de la ressource
data.latestObtenir la dernière valeur résolue (survit au refetch)
data.stateObtenir l’état de la ressource (‘unresolved’, ‘pending’, ‘ready’, ‘errored’)
const { refetch } = dataDéclencher manuellement un refetch
createResource(source, fetcher, { initialValue })Ressource avec valeur initiale
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 ressource refetch automatiquement quand userId() change
  const [user, { refetch, mutate }] = createResource(userId, fetchUser)

  // Mise à jour optimiste
  const updateName = async (newName: string) => {
    const prev = user()
    mutate({ ...prev!, name: newName }) // optimiste
    try {
      await fetch(`/api/users/${userId()}`, {
        method: 'PATCH',
        body: JSON.stringify({ name: newName }),
      })
    } catch {
      mutate(prev) // retour en arrière en cas d'erreur
    }
  }

  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>
  )
}
CommandeDescription
npm install @solidjs/routerInstaller le routeur SolidJS
<Router><Route path="/" component={Home}/></Router>Définir une route basique
<Route path="/users/:id" component={User}/>Route avec paramètre
<Route path="/*all" component={NotFound}/>Route attrape-tout
const params = useParams()Accéder aux paramètres de route
const [searchParams, setSearchParams] = useSearchParams()Accéder aux paramètres de requête
const navigate = useNavigate()Navigation programmatique
navigate('/dashboard')Naviguer vers un chemin
<A href="/about">About</A>Composant de lien de navigation
<A href="/about" activeClass="active">Lien avec style actif
import { Router, Route, A, useParams, useNavigate, useSearchParams } from '@solidjs/router'
import { lazy } from 'solid-js'

// Routes chargées paresseusement
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>
  )
}

// Routes imbriquées
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
}

// Utilisation
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>
  )
}
CommandeDescription
import styles from './App.module.css'Importer des modules CSS
<div class={styles.container}>Appliquer une classe de module CSS
npm install solid-styled-componentsInstaller styled-components pour Solid
const Btn = styled('button')\color: red“Créer un composant stylé
<div class="static-class">Appliquer une classe CSS statique
# Installer 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;
CommandeDescription
npm create solid@latestCréer un projet SolidStart
"use server" directiveMarquer une fonction comme serveur uniquement
createServerAction$()Créer une action serveur
createRouteData()Charger les données pour une route côté serveur
<Title>Page Title</Title>Définir le titre de la page (depuis @solidjs/meta)
<Meta name="description" content="..." />Définir les balises meta
// src/routes/todos.tsx
import { createAsync, query, action, redirect } from '@solidjs/router'

// Requête serveur (chargement de données)
const getTodos = query(async () => {
  'use server'
  const db = await getDatabase()
  return db.todos.findMany()
}, 'todos')

// Action serveur (mutations)
const addTodo = action(async (formData: FormData) => {
  'use server'
  const text = formData.get('text') as string
  const db = await getDatabase()
  await db.todos.create({ data: { text, completed: false } })
  throw redirect('/todos') // revalide
})

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. Toujours appeler les signaux comme des fonctionscount() et non count. Oublier les parenthèses est l’erreur la plus courante avec SolidJS ; sans elles, vous passez la fonction getter au lieu de la valeur.

  2. Ne pas déstructurer les props — La déstructuration casse la réactivité car elle lit la valeur une seule fois. Utilisez props.name directement ou utilisez splitProps()/mergeProps() pour la manipulation des props.

  3. Utiliser <For> pour les tableaux d’objets, <Index> pour les primitives<For> utilise la référence comme clé (meilleur pour les objets qui peuvent être réordonnés) tandis que <Index> utilise la position comme clé (meilleur pour les listes de valeurs simples).

  4. Préférer les stores pour les états complexes — Les signaux sont parfaits pour les valeurs simples, mais pour les objets imbriqués et les tableaux, createStore fournit une réactivité granulaire sans remplacer l’objet entier.

  5. Utiliser on() pour le suivi explicite — Quand vous voulez qu’un effet ne suive que des signaux spécifiques (pas chaque signal lu à l’intérieur), encapsulez avec on(signal, callback).

  6. Tirer parti de Suspense et ErrorBoundary — Encapsulez les composants asynchrones dans Suspense pour les états de chargement et ErrorBoundary pour une gestion gracieuse des erreurs.

  7. Charger paresseusement les routes — Utilisez lazy(() => import('./Page')) pour les composants de route afin d’activer le code splitting et réduire la taille du bundle initial.

  8. Utiliser batch() pour les mises à jour multiples — Quand vous définissez plusieurs signaux à la fois, encapsulez-les dans batch() pour déclencher un seul re-rendu au lieu de plusieurs.

  9. Garder les composants petits — Les composants SolidJS exécutent leur corps une seule fois (pas à chaque rendu comme React), donc découpez les grands composants pour limiter la portée des mises à jour réactives.

  10. Utiliser les génériques TypeScript avec les signauxcreateSignal<string | null>(null) fournit une meilleure sécurité des types et autocomplétion pour votre état réactif.

  11. Comprendre le modèle de compilation — SolidJS compile le JSX en opérations DOM réelles au moment de la compilation. Il n’y a pas de diffing de DOM virtuel, c’est pourquoi c’est rapide mais aussi pourquoi les règles de réactivité (pas de déstructuration, appeler les signaux) sont importantes.

  12. Utiliser untrack() avec parcimonie — Lire un signal à l’intérieur de untrack() empêche le suivi des dépendances. C’est utile pour la journalisation ou les lectures ponctuelles mais peut causer des bugs si utilisé excessivement.