Introduction
SolidJS révolutionne le développement frontend en 2026 grâce à sa réactivité fine-grained, sans virtual DOM ni hooks complexes comme React. Contrairement à React qui re-rend des arbres entiers, SolidJS utilise des signals – des variables réactives primitives – pour updater uniquement les parties impactées du DOM. Cela offre des performances natives, une bundle size réduite et un DX intuitif.
Ce tutoriel intermediate vous guide pour créer une Todo App complète : gestion de liste, filtrage, persistance locale, routing et store global. Pourquoi SolidJS ? Idéal pour apps scalables où la perf compte (dashboards, editors temps réel). Vous partirez d'un setup Vite, implémenterez signals/effets/stores, et optimiserez pour production. À la fin, vous aurez un projet bookmarkable, déployable sur Vercel/Netlify. Prêt à booster vos apps ? (128 mots)
Prérequis
- Node.js 20+ installé
- Connaissances en JavaScript/TypeScript et JSX
- Familiarité avec Vite ou bundlers frontend
- Éditeur comme VS Code avec extension SolidJS
Initialiser le projet Vite + SolidJS
npm create vite@latest todo-solidjs -- --template solid-ts
cd todo-solidjs
npm install
npm install solid-router @solid-primitives/storage
npm run devCette commande crée un projet Vite avec template SolidJS TypeScript, installe les dépendances de base et ajoute solid-router pour le routing + @solid-primitives/storage pour la persistance locale. Lancez npm run dev pour démarrer le serveur sur http://localhost:5173. Évitez les templates non-TS pour la sécurité type.
Structure du projet
Votre dossier ressemble à ceci :
src/: Composants, routes, styles.App.tsx: Point d'entrée.index.html: Injecte le script.
Nous allons remplacer
App.tsx par une Todo App multi-composants. Les signals gèrent l'état local (comme useState mais proxy), les stores l'état global (objets/array réactifs). Prochaine étape : le composant racine avec routing.Configurer le routing et App racine
import { Router } from 'solid-router';
import { lazy } from 'solid-js';
import { routes } from './routes';
const TodoList = lazy(() => import('./components/TodoList'));
const App = () => {
return (
<Router source={window.location.pathname}>
<div class="min-h-screen bg-gradient-to-br from-blue-50 to-indigo-100 p-8">
<header class="text-center mb-12">
<h1 class="text-5xl font-bold bg-gradient-to-r from-blue-600 to-purple-600 bg-clip-text text-transparent mb-4">Todo SolidJS</h1>
<nav class="flex justify-center space-x-4 text-lg">
<a href="/" class="text-blue-600 hover:underline">Accueil</a>
<a href="/about" class="text-blue-600 hover:underline">À propos</a>
</nav>
</header>
<main class="max-w-2xl mx-auto">
<TodoList />
</main>
</div>
</Router>
);
};
export default App;
export { routes };Ce code configure Solid Router pour une SPA avec lazy-loading. Router gère les routes, est le composant principal. Les styles Tailwind (via CDN dans index.html) assurent un design pro. Piège : Oublier lazy augmente le bundle initial ; utilisez-le pour les composants lourds.
Implémenter le store global des Todos
import { createStore } from 'solid-js/store';
import { createStorage } from '@solid-primitives/storage';
export type Todo = {
id: number;
text: string;
completed: boolean;
};
type TodoStore = {
todos: Todo[];
filter: 'all' | 'active' | 'completed';
addTodo: (text: string) => void;
toggleTodo: (id: number) => void;
deleteTodo: (id: number) => void;
setFilter: (filter: TodoStore['filter']) => void;
clearCompleted: () => void;
};
const [state, setState] = createStore({
todos: [] as Todo[],
filter: 'all' as TodoStore['filter'],
});
const persistentState = createStorage({
storage: sessionStorage,
serializer: JSON,
})(state);
export const useTodoStore = (): TodoStore => {
const addTodo = (text: string) => {
const id = Date.now();
setState('todos', (todos) => [...todos, { id, text, completed: false }]);
};
const toggleTodo = (id: number) => {
setState('todos', (todos) =>
todos.map((todo) =>
todo.id === id ? { ...todo, completed: !todo.completed } : todo
)
);
};
const deleteTodo = (id: number) => {
setState('todos', (todos) => todos.filter((t) => t.id !== id));
};
const setFilter = (filter: TodoStore['filter']) => setState('filter', filter);
const clearCompleted = () => {
setState('todos', (todos) => todos.filter((t) => !t.completed));
};
const filteredTodos = () => {
const { todos, filter } = persistentState;
switch (filter()) {
case 'active': return todos.filter((t) => !t.completed);
case 'completed': return todos.filter((t) => t.completed);
default: return todos;
}
};
return {
...persistentState,
filteredTodos,
addTodo,
toggleTodo,
deleteTodo,
setFilter,
clearCompleted,
};
};Le store utilise createStore pour un état global réactif mutable. @solid-primitives/storage persiste en sessionStorage. Les actions comme addTodo updatent via setState avec immutabilité partielle. Piège : Toujours utiliser les fonctions du store pour la réactivité ; mutations directes cassent les updates.
Utilisation des signals et store
Analogie : Un signal est comme un compteur watched ; changer sa valeur trigger uniquement les dependents. Le store étend ça aux objets.
Dans TodoList, on accède au store via useTodoStore(). Les filteredTodos() est un computed proxy qui re-exécute au changement de deps. Prochaine étape : le composant TodoList avec formulaire et liste.
Créer le composant TodoList principal
import { createSignal } from 'solid-js';
import { useTodoStore } from '../stores/todoStore';
export const TodoList = () => {
const [newTodo, setNewTodo] = createSignal('');
const store = useTodoStore();
const handleSubmit = (e: SubmitEvent) => {
e.preventDefault();
if (newTodo().trim()) {
store.addTodo(newTodo().trim());
setNewTodo('');
}
};
return (
<div class="bg-white shadow-2xl rounded-2xl p-8 ring-1 ring-gray-200">
<form onSubmit={handleSubmit} class="mb-8 flex gap-4">
<input
type="text"
value={newTodo()}
onInput={(e) => setNewTodo(e.currentTarget.value)}
placeholder="Ajouter une nouvelle todo..."
class="flex-1 px-4 py-3 border border-gray-300 rounded-xl focus:outline-none focus:ring-4 focus:ring-blue-500 focus:border-transparent transition-all"
/>
<button
type="submit"
class="px-8 py-3 bg-blue-600 text-white font-semibold rounded-xl hover:bg-blue-700 focus:outline-none focus:ring-4 focus:ring-blue-500 transition-all shadow-lg"
>
Ajouter
</button>
</form>
<div class="space-y-3">
{store.filteredTodos().map((todo) => (
<div
key={todo.id}
class="flex items-center p-4 bg-gray-50 rounded-xl hover:bg-gray-100 transition-all border-l-4 border-blue-500"
>
<input
type="checkbox"
checked={todo.completed}
onChange={() => store.toggleTodo(todo.id)}
class="w-5 h-5 text-blue-600 rounded focus:ring-blue-500 mr-4"
/>
<span class={todo.completed ? 'line-through text-gray-500' : 'font-medium'}>
{todo.text}
</span>
<button
onClick={() => store.deleteTodo(todo.id)}
class="ml-auto px-4 py-2 text-red-500 hover:text-red-700 font-semibold transition-colors"
>
Supprimer
</button>
</div>
))}
</div>
{store.todos.length > 0 && (
<div class="mt-8 pt-8 border-t border-gray-200 flex justify-between items-center text-sm text-gray-600">
<span>{store.todos.length} todo(s)</span>
<div class="flex gap-2">
<button onClick={() => store.setFilter('all')} class={store.filter() === 'all' ? 'text-blue-600 font-semibold' : ''}>
Toutes
</button>
<button onClick={() => store.setFilter('active')} class={store.filter() === 'active' ? 'text-blue-600 font-semibold' : ''}>
Actives
</button>
<button onClick={() => store.setFilter('completed')} class={store.filter() === 'completed' ? 'text-blue-600 font-semibold' : ''}>
Terminées
</button>
{store.todos.some((t) => t.completed) && (
<button onClick={store.clearCompleted} class="text-red-500 hover:text-red-700">
Vider terminées
</button>
)}
</div>
</div>
)}
</div>
);
};Ce composant utilise un signal local newTodo pour l'input et le store pour les todos. onInput update le signal, triggerant re-render ciblé. Les maps/listes sont réactives via proxies. Piège : Utilisez onInput pas onChange pour perf ; key unique évite re-renders inutiles.
Ajouter un effet pour stats en temps réel
import { createEffect, Show } from 'solid-js';
import { useTodoStore } from '../stores/todoStore';
export const Stats = () => {
const store = useTodoStore();
createEffect(() => {
console.log(`Todos filtrées: ${store.filteredTodos().length}`);
});
return (
<Show when={store.todos.length > 0}>
<div class="mt-4 p-4 bg-green-50 rounded-xl border border-green-200">
<p class="text-green-800 font-semibold">
{store.filteredTodos().length} / {store.todos.length} tâches {store.filter() === 'completed' ? 'terminées' : 'visibles'}
</p>
</div>
</Show>
);
};createEffect s'exécute après render quand ses deps changent (ici store). conditionne le render. Ajoutez dans TodoList pour le voir. Piège : Évitez les effets side-effects lourds ; ils trackent auto les deps.
Mettre à jour index.html pour Tailwind
<!doctype html>
<html lang="fr">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<script src="https://cdn.tailwindcss.com"></script>
<title>Todo SolidJS 2026</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/entry-client.tsx"></script>
</body>
</html>Ajoute Tailwind CDN pour styles instantanés (prod : PostCSS). entry-client.tsx est le bootstrap SolidJS de Vite. Copiez-collez pour un look pro immédiat.
Bonnes pratiques
- Utilisez toujours des keys uniques dans les listes pour optimiser les reconciliations.
- Préférez les signals/stores aux props drilling : Ils scalent mieux.
- Lazy-load les routes/composants pour bundles <100kb.
- TypeScript strict : Activez
strict: truedans tsconfig.json. - Tests avec Vitest + Testing Library : Couvrez 80% des actions store.
Erreurs courantes à éviter
- Mutation directe d'un store : Utilisez
setStateimmer-like, sinon pas de réactivité. - Oublier les deps dans createEffect : Ajoutez manuellement avec
[dep1, dep2]si auto-track foire. - Signals dans loops : Créez-les en dehors pour éviter memory leaks.
- Pas de cleanup storage : Utilisez localStorage pour persistance forever, session pour sessions.
Pour aller plus loin
- Docs officielles : SolidJS
- Solid Primitives : GitHub pour utils réactifs.
- Déployez sur Vercel :
npm i -g vercel ; vercel.