Dans cette vidéo je vous propose de découvrir SolidJS, une librairie JavaScript qui permet de créer des interfaces utilisateurs réactives.
00:00 Présentation
19:50 Gestion de l'asynchrone
29:11 Mon avis
30:40 Les inconvénients
La particularité de SolidJS
Si on regarde les premiers exemples de la documentation on peut penser que SolidJS n'est qu'un simple clone de React (un peu comme Preact).
import { createSignal } from "solid-js";
export const Compteur = () => {
const [count, setCount] = createSignal(0);
return <div>
<p>Compteur : {count()}</p>
<button onClick={() => setCount(n => n+ 1)}>Incrémenter</button>
</div>;
};
Mais la manière de fonctionner en interne est complètement différent.
Pas de virtual dom
La première grosse particularité et la non utilisation du virtual-DOM. Le JSX est convertit grâce à un module particulier babel-preset-solid
(un plugin vite existe vite-plugin-solid) qui va convertir le code pour générer des éléments HTML directement. On peut par exemple écrire cela :
document.body.appendChild(<div>Hello World</div>)
Si le composant utilise des éléments réactifs les modifications seront faites directement sur le DOM sans étape intermédiaire ce qui permet de bien meilleur performances. L'approche est ici similaire à celle qui est utilisé dans Svelte.
La réactivité
L'autre particularité est la manière qu'a SolidJS de gérer la réactivité. Pour cela Solid utilise un système de signal.
const [count, setCount] = createSignal(0)
Cette méthode renvoie un tableau de 2 éléments :
- Un getter qui, lorsqu'il est appelé, renvoie la valeur courante. Si ce getter est appelé dans un contexte de suivi (
createEffect
par exemple) alors la fonction appelante sera rééxécutée lorsque le Signal est modifié. - Un setter qui permet de changer la valeur à l'intérieur du signal et qui informe les contextes de suivi du changement.
// La fonction sera re-appelée à chaque fois que count sera modifié
createEffect(() => {
console.log(count())
})
En revanche, cette réactivité peut être rendu difficile pour des objets pour lesquels il faut utiliser des signaux imbriqués. Pour éviter trop de lourdeurs SolidJS dispose d'un système de store qui permet de créer un proxy qui génèrera un arbre de signaux automatiquement.
const [todos, setTodos] = createStore([
{
"id": 1,
"title": "delectus aut autem",
"completed": false
},
// ...
])
function addTodo (title) {
setTodos([{title, id: Date.now(), completed: false}, ...todos])
}
function removeTodo (todo) {
setTodos(todos.filter(t => t.id !== todo.id))
}
function toggleTodo (todo) {
// On peut aussi spécifier le chemin
setTodos((t) => todo.id === t.id, 'completed', (completed) => !completed)
}
Le setter des store permet de modifier directement une valeur au sein de l'objet en spécifiant le chemin vers la propriété à modifier ce qui simplifie grandement certaines opérations.
Gestion de l'asynchrone
Un autre aspect intéréssant de SolidJS est sa gestion native de l'asynchrone à plusieurs niveaux. Il est par exemple possible de charger un composant de manière asynchrone gràce à la méthode lazy()
.
import { lazy } from "solid-js"
const ComposantLourd = lazy(() => import("./ComposantLourd"));
function App() {
return (
<>
<h1>Welcome</h1>
<ComposantLourd name="Jake" />
</>
);
}
On peut aussi utiliser la méthode createResource()
qui permet de gérer facilement des données récupérées de manière asynchrones.
import { createSignal, createResource } from "solid-js"
const fetchTodos = (page) => fetch(`https://jsonplaceholder.typicode.com/todos?_limit=5&_page=${page}`).then(r => r.json())
function App () {
const [page, setPage] = createSignal(1)
const [todos] = createResource(page, fetchTodos)
return (
<>
<ul>
<For each={todos}>
{todo => <TodoItem todo={todo}/>}
</For>
</ul>
<button onClick={setPage(n => n + 1)}>Page suivante</button>
</>
)
}
Cette méthode peut aussi être couplée avec un composant <Suspense>
qui permet d'afficher un composant pendant le chargement des données. Composant qui peut aussi être couplé avec un système de transition pour ajouter un effet de chargement lors de nouvelle données arrivent.
import { createSignal, createResource } from "solid-js"
function App () {
const [page, setPage] = createSignal(1)
const [todos] = createResource(page, fetchTodos)
return (
<>
<Suspense fallback={<p>Chargement...</p>}>
<ul>
<For each={todos}>
{todo => <TodoItem todo={todo}/>}
</For>
</ul>
<button onClick={setPage(n => n + 1)}>Page suivante</button>
</Suspense>
</>
)
}
Les inconvénients
Les choix faits par SolidJS ne sont pas non plus vide de compromis. Le suivi des changements de valeur des signaux ne peut se faire que dans un contexte particulier (jsx
ou createEffect
) et dans certaines situation ce suivi ne se fait pas comme on pourrait l'attendre. Cela arrive par exemple si on utilise la destructuration.
function Salutation (props) {
const name = props.name
return <div>
<p>{props.name} sera mis à jour quand la props change</p>
<p>{name} ne sera jamais mis à jour car la destructuration a perdu le contexte de suivi</p>
</div>
}
Au lieu de se retrouver à rechercher la cause de rendus inutiles comme dans React, on se retrouve ici à chercher pourquoi un changement n'est pas suivi correctement. Il faut donc bien comprendre le fonctionnement interne de la librairie pour éviter ce genre de souci quitte parfois à écrire du code qui ne semble pas forcément naturel.
Le second problème est l'utilisation détournée d'une syntaxe classique (le JSX) qui nécessite un module spécifique ce qui peut nuire à la compatibilité avec certains bundlers comme esbuild.
Conclusion
Malgré ces inconvénients SolidJS reste une librairie intéréssante pour créer des éléments d'interfaces spécifique gràce à un poid léger et des performances optimales avec son approche au plus près du DOM. Il faudra tout de même faire attention à sa manière de gérer la réactivité pour voir si elle n'entraine pas trop de surprises par rapport aux autres alternatives.