Cours Svelte et SvelteKit · Module 8: Stores
Stores vs $state
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.js | Stores |
|---|---|---|---|
| Portée | Composant | Toute l'application | Toute l'application |
| Lecture dans le template | {count} | {treasury.gold} | {$gold} |
| Accès hors des composants | Non | Oui | Oui (subscribe, get) |
| Dans le nouveau code | Oui | Oui | Pour 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. Quand vaut-il mieux utiliser Stores plutôt que $state ?