SolidJS
Framework UI JavaScript réactif à grain fin sans DOM virtuel avec des performances exceptionnelles.
Installation
Section intitulée « Installation »Créer un nouveau projet
Section intitulée « Créer un nouveau projet »| Commande | Description |
|---|---|
npx degit solidjs/templates/ts my-app | Créer un projet SolidJS TypeScript |
npx degit solidjs/templates/js my-app | Créer un projet SolidJS JavaScript |
npm create solid@latest | Créer un projet avec le CLI SolidStart |
npm install solid-js | Installer SolidJS dans un projet existant |
npm install solid-start | Installer le méta-framework SolidStart |
Commandes de développement
Section intitulée « Commandes de développement »| Commande | Description |
|---|---|
npm run dev | Démarrer le serveur de développement avec hot reload |
npm run build | Compiler pour la production |
npm run serve | Prévisualiser la compilation de production localement |
npm run start | Démarrer le serveur SolidStart |
Modèles de projets
Section intitulée « Modèles de projets »# 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
Composants et JSX
Section intitulée « Composants et JSX »Composants de base
Section intitulée « Composants de base »| Commande | Description |
|---|---|
function App() { return <div>Hello</div> } | Définir un composant basique |
<MyComponent name="value" /> | Passer des props à un composant |
props.children | Accé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 |
Gestion des événements
Section intitulée « Gestion des événements »| Commande | Description |
|---|---|
<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 |
Modèles de composants
Section intitulée « Modèles de composants »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>
}
Primitives réactives
Section intitulée « Primitives réactives »| Commande | Description |
|---|---|
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é |
Effets et mémos
Section intitulée « Effets et mémos »| Commande | Description |
|---|---|
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 |
Cycle de vie
Section intitulée « Cycle de vie »| Commande | Description |
|---|---|
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 |
Modèles réactifs
Section intitulée « Modèles réactifs »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>
)
}
Flux de contrôle
Section intitulée « Flux de contrôle »Composants intégrés
Section intitulée « Composants intégrés »| Commande | Description |
|---|---|
<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 |
Exemples de flux de contrôle
Section intitulée « Exemples de flux de contrôle »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>
)
}
Stores et état
Section intitulée « Stores et état »Opérations sur les stores
Section intitulée « Opérations sur les stores »| Commande | Description |
|---|---|
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 |
Modèles de stores
Section intitulée « Modèles de stores »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 (/* ... */)
}
Ressources et récupération de données
Section intitulée « Ressources et récupération de données »Opérations sur les ressources
Section intitulée « Opérations sur les ressources »| Commande | Description |
|---|---|
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.loading | Vérifier si la ressource est en cours de chargement |
data.error | Accéder à l’erreur de la ressource |
data.latest | Obtenir la dernière valeur résolue (survit au refetch) |
data.state | Obtenir l’état de la ressource (‘unresolved’, ‘pending’, ‘ready’, ‘errored’) |
const { refetch } = data | Déclencher manuellement un refetch |
createResource(source, fetcher, { initialValue }) | Ressource avec valeur initiale |
Modèles de récupération de données
Section intitulée « Modèles de récupération de données »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>
)
}
@solidjs/router
Section intitulée « @solidjs/router »| Commande | Description |
|---|---|
npm install @solidjs/router | Installer 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 |
Configuration du routeur
Section intitulée « Configuration du routeur »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>
)
}
Contexte et injection de dépendances
Section intitulée « Contexte et injection de dépendances »API Context
Section intitulée « API Context »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>
)
}
Options CSS
Section intitulée « Options CSS »| Commande | Description |
|---|---|
import styles from './App.module.css' | Importer des modules CSS |
<div class={styles.container}> | Appliquer une classe de module CSS |
npm install solid-styled-components | Installer styled-components pour Solid |
const Btn = styled('button')\color: red“ | Créer un composant stylé |
<div class="static-class"> | Appliquer une classe CSS statique |
Intégration Tailwind CSS
Section intitulée « Intégration Tailwind CSS »# 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;
SolidStart (méta-framework)
Section intitulée « SolidStart (méta-framework) »Fonctionnalités de SolidStart
Section intitulée « Fonctionnalités de SolidStart »| Commande | Description |
|---|---|
npm create solid@latest | Créer un projet SolidStart |
"use server" directive | Marquer 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 |
Fonctions serveur et actions
Section intitulée « Fonctions serveur et actions »// 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>
)
}
Configuration Vitest
Section intitulée « Configuration Vitest »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?$/] },
},
})
Tester les composants
Section intitulée « Tester les composants »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()
})
})
Bonnes pratiques
Section intitulée « Bonnes pratiques »-
Toujours appeler les signaux comme des fonctions —
count()et noncount. Oublier les parenthèses est l’erreur la plus courante avec SolidJS ; sans elles, vous passez la fonction getter au lieu de la valeur. -
Ne pas déstructurer les props — La déstructuration casse la réactivité car elle lit la valeur une seule fois. Utilisez
props.namedirectement ou utilisezsplitProps()/mergeProps()pour la manipulation des props. -
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). -
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,
createStorefournit une réactivité granulaire sans remplacer l’objet entier. -
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 avecon(signal, callback). -
Tirer parti de
SuspenseetErrorBoundary— Encapsulez les composants asynchrones dansSuspensepour les états de chargement etErrorBoundarypour une gestion gracieuse des erreurs. -
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. -
Utiliser
batch()pour les mises à jour multiples — Quand vous définissez plusieurs signaux à la fois, encapsulez-les dansbatch()pour déclencher un seul re-rendu au lieu de plusieurs. -
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.
-
Utiliser les génériques TypeScript avec les signaux —
createSignal<string | null>(null)fournit une meilleure sécurité des types et autocomplétion pour votre état réactif. -
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.
-
Utiliser
untrack()avec parcimonie — Lire un signal à l’intérieur deuntrack()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.