Cours Svelte et SvelteKit · Module 8: Stores

Stores vs $state

3 min de lecture ·
Dans cette leçon6

En Svelte 5, tu peux garder l'état partagé dans des stores ou dans des runes. Quand choisir l'un ou l'autre ?

$state - État du composant

<script>
  // État local du composant
  let count = $state(0);
  let items = $state([]);
</script>

À utiliser quand :

  • L'état est local au composant
  • Tu n'as pas besoin d'y accéder depuis d'autres composants
  • Les données n'ont pas besoin de survivre au démontage

$state dans un fichier .svelte.js - état partagé

Les runes fonctionnent aussi hors des composants, dans les fichiers qui se terminent par .svelte.js (ou .svelte.ts). C'est la façon recommandée par Svelte 5 pour l'état partagé dans le nouveau code :

// treasury.svelte.js
export const treasury = $state({ gold: 0 });

export function addGold(amount) {
  treasury.gold += amount;
}
<!-- Chest.svelte -->
<script>
  import { treasury, addGold } from './treasury.svelte.js';
</script>

<p>Or : {treasury.gold}</p>
<button onclick={() => addGold(10)}>+10 pièces d'or</button>

Chaque composant qui importe treasury voit le même or, sans $ devant le nom et sans abonnement. Exporte un objet et modifie ses champs : une variable $state à laquelle tu assignes une nouvelle valeur ne peut pas être exportée directement (le compilateur signale une erreur).

Dans SvelteKit, un tel module est unique pour tout le serveur : transmets donc les données d'un utilisateur précis par le contexte (setContext, leçon 4_5), et non par le module.

Stores - toujours d'actualité

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

export const user = writable(null);
export const cart = writable([]);

À utiliser quand :

  • Tu as des flux de données asynchrones complexes (WebSocket, minuteurs)
  • Tu as besoin de contrôler manuellement l'abonnement (start et stop dans readable)
  • Une bibliothèque ou du code plus ancien fournit un store

Context + $state

<!-- Provider.svelte -->
<script>
  import { setContext } from 'svelte';

  let { children } = $props();

  let state = $state({ user: null, theme: 'dark' });

  setContext('app', {
    get state() { return state; },
    setUser: (user) => state.user = user,
    toggleTheme: () => state.theme = state.theme === 'dark' ? 'light' : 'dark'
  });
</script>

{@render children()}

Comparaison

Fonctionnalité$state dans un composant$state dans .svelte.jsStores
PortéeComposantToute l'applicationToute l'application
Lecture dans le template{count}{treasury.gold}{$gold}
Accès hors des composantsNonOuiOui (subscribe, get)
Dans le nouveau codeOuiOuiPour les flux et les bibliothèques

Combiner stores et runes

Tu peux lire un store avec le préfixe $ aussi dans les runes, par exemple dans $derived :

<script>
  import { warriors } from './stores.js';

  // $derived se recalcule quand le store change
  let activeWarriors = $derived($warriors.filter(w => w.active));
</script>

<p>Guerriers actifs : {activeWarriors.length}</p>

Calcule les valeurs issues d'un store avec $derived, et non avec un $effect qui assigne : le résultat est toujours à jour et il n'y a aucun risque de boucle d'effets.

Code de cette leçon : App.svelte
<script>
  // Svelte 5 : $state, $state dans un fichier .svelte.js et stores - quand utiliser quoi ?
  import { globalCount } from './stores.js';
  import { shared } from './shared.svelte.js';
  import Watcher from './Watcher.svelte';

  // État local - uniquement dans ce composant
  let localCount = $state(0);
</script>

<main class="comparison">
  <h1>$state vs Stores</h1>

  <section>
    <h2>$state (local)</h2>
    <p>Valeur : <strong>{localCount}</strong></p>
    <button onclick={() => localCount++}>+1 localement</button>
    <p class="hint">Uniquement dans ce composant</p>
  </section>

  <section>
    <h2>$state dans shared.svelte.js (partagé)</h2>
    <p>Valeur : <strong>{shared.count}</strong></p>
    <button onclick={() => shared.count++}>+1 partagé</button>
    <p class="hint">La façon Svelte 5 pour le nouveau code - sans $ ni abonnement</p>
  </section>

  <section>
    <h2>Store (partagé)</h2>
    <p>Valeur : <strong>{$globalCount}</strong></p>
    <button onclick={() => globalCount.update(n => n + 1)}>+1 dans le store</button>
    <p class="hint">Toujours valable : flux de données, bibliothèques, ancien code</p>
  </section>

  <Watcher />

  <section class="guidelines">
    <h2>Quand utiliser quoi ?</h2>
    <div class="guideline">
      <h3>$state</h3>
      <ul>
        <li>État local du composant</li>
        <li>État partagé dans les fichiers .svelte.js</li>
        <li>Formulaires, état de l’UI (survol, ouverture)</li>
      </ul>
    </div>
    <div class="guideline">
      <h3>Stores</h3>
      <ul>
        <li>Flux de données asynchrones</li>
        <li>Contrôle manuel des abonnements</li>
        <li>Bibliothèques et ancien code</li>
      </ul>
    </div>
  </section>
</main>

<style>
  .comparison {
    font-family: 'Georgia', serif;
    max-width: 600px;
    margin: 0 auto;
    padding: 2rem;
    background: #1a0f0a;
    color: #e8d5b7;
    min-height: 100vh;
  }

  h1 {
    color: #d4af37;
    text-align: center;
  }

  section {
    background: rgba(212, 175, 55, 0.1);
    border: 1px solid #d4af37;
    border-radius: 8px;
    padding: 1rem;
    margin: 1rem 0;
  }

  h2 {
    color: #d4af37;
    font-size: 1rem;
    margin-top: 0;
  }

  strong {
    color: #ffd700;
    font-size: 1.5rem;
  }

  button {
    padding: 0.5rem 1rem;
    background: #3d2914;
    color: #e8d5b7;
    border: 1px solid #d4af37;
    border-radius: 4px;
    cursor: pointer;
  }

  .hint {
    font-size: 0.85rem;
    color: #8b7355;
    font-style: italic;
    margin-top: 0.5rem;
  }

  .guidelines {
    display: grid;
    grid-template-columns: 1fr 1fr;
    gap: 1rem;
  }

  .guidelines h2 {
    grid-column: 1 / -1;
  }

  .guideline {
    background: rgba(0,0,0,0.3);
    padding: 1rem;
    border-radius: 4px;
  }

  .guideline h3 {
    color: #d4af37;
    margin: 0 0 0.5rem 0;
    font-size: 1rem;
  }

  .guideline ul {
    margin: 0;
    padding-left: 1.2rem;
  }

  .guideline li {
    margin: 0.25rem 0;
    font-size: 0.9rem;
  }
</style>

Entrées de la documentation utilisées dans cette leçon

Tu as repéré une erreur dans cette leçon ?

Teste tes connaissances

Réponds aux questions de cette leçon. Choisis une réponse pour voir tout de suite si elle est correcte.

  1. 1. Quand vaut-il mieux utiliser Stores plutôt que $state ?

Articles utiles