Maîtriser SvelteKit : Développement Web Réactif et Ultra-Performant
Maîtriser SvelteKit : Développement Web Réactif et Ultra-Performant

Gestion de l'État avec les Stores Svelte et SvelteKit

Dans le cadre du cours "Maîtriser SvelteKit : Développement Web Réactif et Ultra-Performant", cette leçon vous plongera au cœur de la gestion de l'état dans les applications Svelte et SvelteKit. Nous explorerons comment les stores Svelte, une fonctionnalité native et élégante, permettent de partager et de synchroniser des données à travers votre application de manière réactive et performante.

Introduction à la Gestion de l'État

Dans toute application web moderne, la gestion de l'état (ou state management) est un défi central. L'état représente toutes les données qui changent au fil du temps et qui doivent être partagées entre différentes parties de l'application. Cela peut inclure :

  • Les données utilisateur (nom, identifiant de session)
  • Les préférences de l'interface utilisateur (thème clair/sombre, langue)
  • Les résultats d'appels API
  • L'état d'un formulaire, etc.

Sans un mécanisme clair de gestion de l'état, les applications deviennent rapidement difficiles à maintenir. Les données se propagent via des "props" descendantes (prop drilling), ou des événements remontent la hiérarchie des composants, rendant le code spaghetti et sujet aux erreurs.

Svelte, fidèle à sa philosophie de simplicité et d'efficacité, offre une solution native et puissante pour la gestion de l'état global : les stores. Contrairement à d'autres frameworks qui nécessitent des bibliothèques tierces complexes (comme Redux pour React ou Vuex pour Vue), Svelte intègre cette fonctionnalité directement, avec une API minimaliste et intuitive.

Qu'est-ce qu'un Store Svelte ?

Un store Svelte est simplement un objet avec une méthode subscribe. C'est l'interface minimale requise pour qu'une variable Svelte puisse être réactive. En pratique, les stores vous permettent de créer des sources de données réactives qui peuvent être souscrites par n'importe quel composant dans votre application. Lorsqu'un store change, tous les composants qui y sont souscrits sont automatiquement mis à jour.

Les stores sont parfaits pour gérer :

  • L'état global de l'application.
  • L'état qui doit être partagé entre des composants non directement liés.
  • Des données qui persistent au-delà de la durée de vie d'un composant individuel.

Les Types de Stores Svelte

Svelte propose trois types de stores principaux, chacun ayant ses propres cas d'utilisation :

1. writable Stores

Le store writable est le type de store le plus couramment utilisé. Il permet de stocker des valeurs qui peuvent être lues et modifiées.

  • Création : writable(initialValue)
  • Méthodes :
    • .set(newValue) : Définit une nouvelle valeur pour le store, écrasant l'ancienne.
    • .update(updaterFunction) : Met à jour la valeur du store en utilisant une fonction qui reçoit la valeur actuelle et retourne la nouvelle valeur. Utile pour des mises à jour basées sur l'état précédent.

2. readable Stores

Un store readable est un store en lecture seule. Sa valeur ne peut être définie qu'au moment de sa création ou par un callback interne au store lui-même. C'est utile pour des données qui proviennent d'une source externe et ne sont pas censées être modifiées par l'interface utilisateur (par exemple, l'heure actuelle, des données d'API statiques, etc.).

  • Création : readable(initialValue, startFunction)
    • startFunction est un callback optionnel appelé quand le premier abonné souscrit au store. Il doit retourner une fonction stop qui sera appelée quand le dernier abonné se désabonne.

3. derived Stores

Un store derived (dérivé) permet de créer un nouveau store dont la valeur dépend d'un ou plusieurs autres stores. Lorsque les stores dont il dépend changent, le store dérivé est automatiquement recalculé et mis à jour. C'est idéal pour des données calculées ou agrégées.

  • Création : derived(stores, callbackFunction, initialValue)
    • stores : Peut être un store unique ou un tableau de stores.
    • callbackFunction : Une fonction qui reçoit la/les valeur(s) des stores dépendants et retourne la nouvelle valeur du store dérivé.
    • initialValue (optionnel) : La valeur initiale du store dérivé avant que les dépendances ne soient prêtes.

Utilisation des Stores en Pratique

L'utilisation des stores Svelte est incroyablement simple grâce à leur réactivité intrinsèque et à la syntaxe concise de Svelte.

1. Le Store writable simple : un Compteur

Commençons par créer un store writable pour gérer un simple compteur.

Créez un fichier src/lib/stores/compteur.js (la convention src/lib/stores est courante pour organiser les stores) :

// src/lib/stores/compteur.js
import { writable } from 'svelte/store';

// Création d'un store writable avec une valeur initiale de 0
export const compteur = writable(0);

// Vous pouvez également exporter des fonctions pour modifier le store
// Cela centralise la logique de modification du store
export const increment = () => compteur.update(n => n + 1);
export const decrement = () => compteur.update(n => n - 1);
export const reset = () => compteur.set(0);

Maintenant, utilisons ce store dans un composant Svelte, par exemple src/routes/+page.svelte :

<!-- src/routes/+page.svelte -->
<script lang="ts">
  import { compteur, increment, decrement, reset } from '$lib/stores/compteur';

  // L'opérateur $ (dollarsign) est une fonctionnalité Svelte géniale !
  // Il "déréférence" automatiquement le store et rend sa valeur réactive.
  // Quand $compteur change, le DOM est mis à jour.
  // Pas besoin de .subscribe() ou de .unsubscribe() manuels !
</script>

<h1>Compteur Svelte</h1>

<p>Valeur actuelle du compteur : <strong>{$compteur}</strong></p>

<button on:click={increment}>Incrémenter</button>
<button on:click={decrement}>Décrémenter</button>
<button on:click={reset}>Réinitialiser</button>

<style>
  button {
    margin: 5px;
    padding: 10px 20px;
    font-size: 1em;
    cursor: pointer;
  }
</style>

Explication du code :

  • Nous importons le store compteur et les fonctions increment, decrement, reset depuis notre fichier de store.
  • L'élément clé est l'utilisation de {$compteur}. Svelte reconnaît le préfixe $ devant une variable et comprend qu'il s'agit d'un store. Il génère automatiquement le code JavaScript nécessaire pour souscrire au store au montage du composant et pour se désabonner à sa destruction. À chaque fois que la valeur du store compteur change (via set ou update), le texte affichant {$compteur} est automatiquement mis à jour.
  • Les boutons appellent les fonctions increment, decrement, reset que nous avons exportées de notre fichier de store, ce qui modifie la valeur du store compteur.

2. Le Store derived : État Calculé

Continuons avec l'exemple du compteur et créons un store dérivé qui nous dira si la valeur du compteur est paire ou impaire.

Mettez à jour src/lib/stores/compteur.js :

// src/lib/stores/compteur.js
import { writable, derived } from 'svelte/store';

export const compteur = writable(0);

export const increment = () => compteur.update(n => n + 1);
export const decrement = () => compteur.update(n => n - 1);
export const reset = () => compteur.set(0);

// Nouveau store dérivé
export const estPair = derived(
  compteur, // Le store dont dépend 'estPair'
  ($compteur) => $compteur % 2 === 0 // La fonction qui calcule la nouvelle valeur
);

Maintenant, utilisez ce nouveau store estPair dans src/routes/+page.svelte :

<!-- src/routes/+page.svelte -->
<script lang="ts">
  import { compteur, increment, decrement, reset, estPair } from '$lib/stores/compteur';
</script>

<h1>Compteur Svelte</h1>

<p>Valeur actuelle du compteur : <strong>{$compteur}</strong></p>
<p>Le nombre est : <strong>{$estPair ? 'Pair' : 'Impair'}</strong></p>

<button on:click={increment}>Incrémenter</button>
<button on:click={decrement}>Décrémenter</button>
<button on:click={reset}>Réinitialiser</button>

<style>
  button {
    margin: 5px;
    padding: 10px 20px;
    font-size: 1em;
    cursor: pointer;
  }
</style>

Explication du code :

  • Nous avons créé estPair en utilisant derived(compteur, ($compteur) => $compteur % 2 === 0). Cela signifie que estPair dépend de compteur. À chaque fois que compteur change, la fonction ($compteur) => $compteur % 2 === 0 est exécutée avec la nouvelle valeur de compteur, et le résultat devient la nouvelle valeur de estPair.
  • Dans le composant, nous utilisons {$estPair} de la même manière que {$compteur}. Svelte gère la réactivité automatiquement.

3. Le Store readable : l'Heure Actuelle

Pour illustrer un store readable, créons un store qui diffuse l'heure actuelle chaque seconde.

Créez un fichier src/lib/stores/horloge.js :

// src/lib/stores/horloge.js
import { readable } from 'svelte/store';

// Création d'un store readable
export const horloge = readable(new Date(), function start(set) {
  // Cette fonction est appelée quand le premier composant s'abonne
  const interval = setInterval(() => {
    set(new Date()); // Met à jour la valeur du store
  }, 1000); // Toutes les secondes

  // Cette fonction est retournée et appelée quand le dernier composant se désabonne
  return function stop() {
    clearInterval(interval);
  };
});

Utilisez-le dans un composant, par exemple src/routes/horloge/+page.svelte :

<!-- src/routes/horloge/+page.svelte -->
<script lang="ts">
  import { horloge } from '$lib/stores/horloge';
</script>

<h1>Heure Actuelle</h1>

<p>Il est actuellement : <strong>{$horloge.toLocaleTimeString()}</strong></p>

Explication du code :

  • Le store horloge est créé avec readable(new Date(), function start(set){...}).
  • La fonction start est exécutée une seule fois lorsque le premier composant s'abonne à horloge. Elle met en place un setInterval qui appelle set(new Date()) chaque seconde, mettant à jour la valeur du store.
  • La fonction stop est retournée par start. Elle est appelée quand le dernier composant s'est désabonné de horloge, ce qui permet de nettoyer la ressource (ici, le setInterval) et d'éviter les fuites de mémoire.

Gestion de l'État dans SvelteKit

SvelteKit, étant construit sur Svelte, hérite naturellement de la capacité à utiliser les stores. Il y a quelques considérations spécifiques à SvelteKit :

1. Organisation des Stores

La convention de placer les stores dans src/lib/stores/ est une excellente pratique. src/lib est un alias par défaut dans SvelteKit qui pointe vers src/lib, facilitant les importations comme $lib/stores/compteur.js. Cela centralise votre logique de gestion d'état et la rend facilement importable n'importe où dans votre application.

2. Interaction avec les Layouts

Étant donné que les layouts (fichiers +layout.svelte) persistent à travers les navigations de pages et encapsulent l'interface utilisateur, ils sont des endroits idéaux pour injecter et consommer des stores globaux qui affectent l'ensemble de l'application (par exemple, un store d'authentification, un store de thème). Un store défini dans un layout peut être souscrit par n'importe quel composant enfant de ce layout.

3. Considérations pour le SSR (Server-Side Rendering)

SvelteKit gère le SSR, ce qui signifie que vos composants et vos stores peuvent être initialisés côté serveur avant d'être hydratés côté client.

  • Initialisation : Lorsque vos stores sont importés et utilisés dans un composant qui est rendu sur le serveur, leur valeur initiale est celle définie dans le store.
  • Réactivité : La réactivité des stores (c'est-à-dire les mises à jour et les abonnements) ne fonctionne que côté client (dans le navigateur). Sur le serveur, les stores sont simplement des objets avec leur valeur actuelle.
  • États persistants vs. transitoires : Soyez conscient que les stores peuvent maintenir leur état entre les requêtes sur le serveur si vous ne les réinitialisez pas correctement ou si vous les importez dans un contexte global serveur. Pour les données spécifiques à une requête (session, user), il est souvent préférable d'utiliser les fonctions load de SvelteKit (+page.server.js ou +layout.server.js) qui fournissent des données à la page ou au layout, plutôt que de tenter de gérer un état mutable global avec des stores Svelte standards côté serveur. Les stores Svelte sont principalement conçus pour la gestion de l'état client-side.

Bonnes Pratiques et Cas Avancés

  • Convention du $store : Adoptez systématiquement la syntaxe $store pour accéder aux valeurs des stores dans vos composants. C'est la manière idiomatique et la plus performante de travailler avec les stores Svelte, car elle gère automatiquement les abonnements et les désabonnements.
  • Séparation des préoccupations : Gardez la logique de vos stores (comment ils sont mis à jour, les validations, les appels API) séparée de la logique de présentation de vos composants. Exportez des fonctions qui encapsulent les opérations sur vos stores, comme nous l'avons fait avec increment et decrement.
  • Stores personnalisés (Custom Stores) : Pour des cas d'utilisation plus complexes (par exemple, l'intégration avec le localStorage, des websockets, des systèmes de caches), vous pouvez créer vos propres stores personnalisés en implémentant l'interface minimale (subscribe et éventuellement set/update si c'est un store modifiable).
  • Stores vs. Context API :
    • Les stores sont idéaux pour un état global qui doit être accessible de n'importe où dans l'arbre des composants, ou même en dehors des composants (dans des modules utilitaires).
    • L'API Context de Svelte est meilleure pour partager des données avec un sous-arbre spécifique de composants, évitant le "prop drilling" sans rendre l'état global. C'est un peu comme un "mini-store" scoped à une partie de l'application.

Conclusion

Les stores Svelte représentent une solution élégante, simple et incroyablement puissante pour la gestion de l'état dans vos applications. Leur intégration native dans le framework, combinée à la syntaxe concise de Svelte (notamment l'opérateur $), élimine la complexité souvent associée à la gestion de l'état dans d'autres écosystèmes.

En maîtrisant les stores writable, readable et derived, vous êtes bien équipé pour construire des applications Svelte et SvelteKit réactives, maintenables et ultra-performantes, capables de gérer n'importe quel type de données partagées avec aisance. Vous avez maintenant les outils pour centraliser et coordonner l'état de votre application de manière fluide et intuitive.