SolidJS 2.0 est async first : qu'est ce que ça signifie ?
La première version beta de SolidJS 2.0 est sortie la semaine dernière. Ryan Carniato a fait un stream pour expliquer en quoi on passait d'une version sync first à une version async first. Il a notamment présenté plusieurs exemples qui démontrent la simplicité de l'utilisation des expressions asynchrones dans SolidJS 2.0. On va voir ici un cas d'utilisation possible sur la récupération d'une liste paginée.
Spécificités de l'asynchronisme
Quand on récupère une donnée de manière synchrone, on n'a pas de question à se poser, la donnée est disponible et on l'utilise. Pour une donnée asynchrone par contre il y a plusieurs problèmes :
- avant de récupérer la donnée pour la première fois, la donnée n'est pas initialisée
- il y a un temps d'attente avant de récupérer la donnée pendant lequel on peut vouloir afficher un indicateur visuel à l'utilisateur
- quand on met à jour la donnée (par exemple passage de la première à la deuxième page d'une liste) on va en général vouloir continuer d'afficher les données de la première page tant que les données de la deuxième page ne sont pas disponibles mais aussi afficher un indicateur pour dire qu'on est en train de récupérer les données suivantes
Il y a également une problématique de cohérence, si un résultat d'une opération asynchrone dépend d'une donnée synchrone (liée à une saisie par l'utilisateur par exemple) on veut en général n'afficher la nouvelle valeur de la donnée synchrone que lorsque le calcul de la donnée asynchrone est terminé pour avoir une incohérence entre les deux.
Comment cela est géré jusqu'à présent
Jusqu'à présent, le fonctionnement spécifique de l'asynchronisme faisait que l'on avait un traitement différent entre la donnée synchrone et la donnée asynchrone. On a vu par exemple émerger Tanstack Query pour cela. L'arrivée du hook use en React a également permis de simplifier la gestion des données asynchrones.
Au niveau de SolidJS 1.0 on a createSignal pour les données synchrones et createResource pour les données asynchrones.
La gestion des données asynchrones se simplifie au cours du temps grâce à de nouvelles primitives mais on a quand même un code spécifique, c'est en cela que l'on n'est pas async first.
Le changement de paradigme
L'idée avec Solid 2.0 c'est d'avoir un paradigme qui traite les données synchrones et asynchrones de la même manière. L'idée derrière cela c'est de se dire que si j'ai du code qui fonctionne avec des données synchrones, le même code fonctionne aussi avec des données asynchrones.
Dans un des exemple de Ryan on a un code de ce type
function App() {
const [value, setValue] = createSignal(1);
const increment = () => setValue((prev) => prev + 1);
const result = createMemo(() => compute(value()));
return (
<div>
<button type="button" onClick={increment}>{latest(value)}</button>
<p class={[{ pending: isPending(result) }]}>Result for {value()} = {result().toFixed(3)}</p>
</div>
);
}
Ce code fonctionne de la même manière que la fonction compute soit une fonction synchrone ou asynchrone. Dans les deux cas, la fonction createMemo va retourner un type Accessor<number>.
Un point important à noter est que la valeur value ne sera mise à jour que lorsque la propriété dérivée result aura pu être calculée et donc quand compute aura retourné un résultat. L'idée c'est d'avoir une consistance entre toutes les données pour éviter d'afficher un résultat qui ne correspond pas à la valeur courante.
Pour traiter les besoins spécifiques à l'asynchrone Solid va fournir plusieurs éléments
- on a un composant
Loadingqui correspond auSuspensede React ou de Solid 1.0 qui va afficher un fallback lorsque la donnée n'est pas encore initialisée (au premier appel). Ce fallback ne sera par contre pas affiché quand on récupérera une nouvelle valeur - le helper
isPendingva permettre de savoir si un refetch est en cours, dans notre exemple on pourrait faireisPending(result)pour savoir si on est en cours de calcul - le helper
latest(value)va permettre d'avoir accès à la nouvelle valeur devalueavant d'avoir la réponse deresult, cela permet d'afficher cette valeur dès que l'on clique au niveau du bouton
Cas d'usage : liste paginée
A partir de ce nouveau mode de fonctionnement on peut très facilement implémenter une liste paginée avec le code suivant
const App = () => {
const [page, setPage] = createSignal(1);
const result = createMemo(() => getResults(page()));
return (
<div>
<h1>Demo</h1>
<button disabled={latest(page) === 1} onClick={() => setPage(prev => prev - 1)}>Previous</button>
Page {latest(page)}
<button onClick={() => setPage(prev => prev + 1)}>Next</button>
<h2>Result </h2>
<Loading fallback="Loading...">
<div class={[{loading: isPending(result)}]}>
<For each={result()}>
{(value) => <p>{value()}</p>}
</For>
<p>{result().length} items</p>
</div>
</Loading>
</div>
);
};
On a automatiquement un loader au premier chargement de la liste puis la classe loading qui sera appliqué à chaque changement de page.
Conclusion
Cet exemple simple présente les apports du nouveau paradigme “Async First” de SolidJS 2.0. On pourra voir par la suite que cela amène d'autres avantages, notamment la possibilité d'initialiser les signaux avec une fonction réactive.
Par rapport à Solid 1.0, on simplifie l'API du framework puisque l'on utilise systématiquement createSignal plutôt que d'utiliser une autre API createResource pour les données asynchrones.
L'intégration de Solid 2.0 avec SolidStart n'est pas encore disponible mais cela veut également dire que createAsync va disparaître et que l'on pourra utiliser createMemo pour appeler une server function.