Cours Svelte et SvelteKit · Module 3: Réactivité et Runes
$effect - Effets de bord
Dans cette leçon9
$effect est une rune qui exécute du code en réaction aux changements. Utilise-la pour synchroniser avec le monde extérieur.
Utilisation de base
<script>
let count = $state(0);
// S'exécute à chaque changement de count
$effect(() => {
console.log('Counter:', count);
});
</script>
<button onclick={() => count++}>
Increase ({count})
</button>Quand utiliser $effect ?
Bons cas d'usage :
- Logging/débogage
- Synchronisation avec localStorage
- Intégration avec des bibliothèques externes
- Écoute d'événements
- Minuteries et animations
À éviter :
- Mettre à jour un autre état (utilise
$derived) - Récupérer des données (utilise
loaddans SvelteKit)
Exemple : localStorage
<script>
// Charge le thème sauvegardé au démarrage (ou utilise celui par défaut)
let theme = $state(localStorage.getItem('theme') ?? 'dark');
// Sauvegarde dans localStorage à chaque changement
$effect(() => {
localStorage.setItem('theme', theme);
document.body.className = theme;
});
</script>
<select bind:value={theme}>
<option value="dark">Dark (Northern Night)</option>
<option value="light">Light (Day of the Sun)</option>
</select>On lit le thème sauvegardé directement dans la valeur initiale de $state, et l'effet ne fait que sauvegarder les changements. Ne sépare pas cela en deux effets, l'un qui sauvegarde et l'autre qui charge : la sauvegarde s'exécuterait en premier et écraserait le thème mémorisé. Dans SvelteKit, un composant est aussi rendu sur le serveur, où localStorage n'existe pas : utilise-le seulement dans le navigateur, par exemple dans onMount (Valhalla, module 7).
Nettoyage (Cleanup)
$effect peut retourner une fonction de nettoyage :
<script>
let isRunning = $state(false);
let seconds = $state(0);
$effect(() => {
if (!isRunning) return;
// Définit l'intervalle
const interval = setInterval(() => {
seconds += 1;
}, 1000);
// Nettoyage - efface au prochain appel
return () => {
clearInterval(interval);
};
});
</script>
<p>Time: {seconds}s</p>
<button onclick={() => isRunning = !isRunning}>
{isRunning ? 'Stop' : 'Start'}
</button>$effect.pre - Avant la mise à jour du DOM
<script>
let messages = $state([]);
// S'exécute AVANT la mise à jour du DOM
$effect.pre(() => {
console.log('Before render, messages:', messages.length);
});
</script>Dépendances des effets
$effect suit automatiquement les dépendances :
<script>
let a = $state(1);
let b = $state(2);
let c = $state(3);
$effect(() => {
// Cet effet s'exécutera quand a ou b changent
// Il ne s'exécutera PAS quand c change
console.log('a + b =', a + b);
});
</script>untrack - Lire sans suivre
Parfois, un effet doit lire un état sans en dépendre. Exemple typique : un journal des changements. L'effet réagit à count et ajoute une entrée au tableau logs. S'il lisait logs normalement, il se relancerait lui-même sans fin, et Svelte l'arrêterait avec l'erreur effect_update_depth_exceeded. La fonction untrack lit l'état sans le suivre :
<script>
import { untrack } from 'svelte';
let count = $state(0);
let logs = $state([]);
$effect(() => {
const entry = `Compteur : ${count}`; // dépendance : count
untrack(() => {
logs = [...logs, entry].slice(-5); // logs lu sans suivi
});
});
</script>
<button onclick={() => count++}>+1</button>
<ul>
{#each logs as log}
<li>{log}</li>
{/each}
</ul>Garde quand même la règle en tête : si une valeur peut être calculée à partir d'autres, utilise $derived, pas un effet.
$inspect - Suivre dans la console
Pour déboguer, au lieu d'un effet avec console.log, tu peux utiliser la rune $inspect. Elle affiche la valeur dans la console au démarrage et après chaque changement, même tout au fond d'un objet. Elle ne fonctionne qu'en mode développement : elle disparaît de l'application construite :
<script>
let warrior = $state({ name: 'Ragnar', health: 100 });
$inspect(warrior); // affiche tout l'objet après chaque changement
</script>
<button onclick={() => warrior.health -= 10}>
{warrior.name} : {warrior.health} PV
</button>Exemple : Intégration avec une bibliothèque externe
<script>
import Chart from 'chart.js/auto';
let loot = $state([10, 20, 30, 40]);
let canvas;
$effect(() => {
// L'effet lit loot, il se relance donc après chaque changement
const chart = new Chart(canvas, {
type: 'bar',
data: {
labels: ['Ragnar', 'Bjorn', 'Ivar', 'Ubbe'],
datasets: [{ label: 'Butin', data: $state.snapshot(loot) }]
}
});
// Nettoyage - détruit l'ancien graphique avant d'en créer un nouveau
return () => chart.destroy();
});
</script>
<canvas bind:this={canvas}></canvas>
<button onclick={() => loot[0] += 10}>Ragnar prend du butin</button>L'effet démarre après le montage du composant, donc canvas est déjà prêt. $state.snapshot(loot) donne à la bibliothèque une simple copie du tableau au lieu d'un proxy réactif : les bibliothèques externes préfèrent des données ordinaires. Chaque changement du butin détruit l'ancien graphique dans la fonction de nettoyage et en dessine un nouveau.
Code de cette leçon : App.svelte
<script>
// $effect - side effects
import { untrack } from 'svelte';
let theme = $state('dark');
let count = $state(0);
let logs = $state([]);
// Logging effect: depends on count only.
// logs is read inside untrack - otherwise the effect would
// depend on logs, change it and re-run itself forever.
$effect(() => {
const entry = `Count: ${count}`;
untrack(() => {
logs = [...logs, entry].slice(-5);
});
});
// Effect with cleanup
let seconds = $state(0);
let isRunning = $state(false);
$effect(() => {
if (!isRunning) return;
const interval = setInterval(() => {
seconds += 1;
}, 1000);
// Cleanup - return a function
return () => clearInterval(interval);
});
// Effect on document
$effect(() => {
document.body.className = theme;
});
</script>
<main class="effects-demo">
<h1>$effect - Side effects</h1>
<section>
<h2>Logging effect</h2>
<p>Count: {count}</p>
<button onclick={() => count++}>+1</button>
<div class="logs">
<h3>Logs (last 5):</h3>
{#each logs as log}
<p>{log}</p>
{/each}
</div>
</section>
<section>
<h2>Effect with cleanup (Timer)</h2>
<p>Seconds: <strong>{seconds}</strong></p>
<button onclick={() => isRunning = !isRunning}>
{isRunning ? '⏹Stop' : '▶Start'}
</button>
<button onclick={() => seconds = 0}>Reset</button>
<p class="hint">Cleanup stops the interval when isRunning = false</p>
</section>
<section>
<h2>Effect on document</h2>
<select bind:value={theme}>
<option value="dark">Dark</option>
<option value="light">Light</option>
</select>
<p class="hint">Changes body class</p>
</section>
</main>
<style>
.effects-demo {
font-family: 'Georgia', serif;
max-width: 600px;
margin: 0 auto;
padding: 2rem;
background: #1a0f0a;
color: #e8d5b7;
}
h1, h2 {
color: #d4af37;
}
section {
background: rgba(212, 175, 55, 0.1);
border: 1px solid #d4af37;
border-radius: 8px;
padding: 1rem;
margin-bottom: 1rem;
}
button {
padding: 0.5rem 1rem;
background: #3d2914;
color: #e8d5b7;
border: 1px solid #d4af37;
border-radius: 4px;
cursor: pointer;
margin-right: 0.5rem;
}
select {
padding: 0.5rem;
background: #2c1810;
color: #e8d5b7;
border: 1px solid #d4af37;
border-radius: 4px;
}
.logs {
margin-top: 1rem;
padding: 0.5rem;
background: rgba(0,0,0,0.3);
border-radius: 4px;
}
.logs h3 {
margin: 0 0 0.5rem 0;
font-size: 0.9rem;
color: #8b7355;
}
.logs p {
margin: 0.25rem 0;
font-size: 0.85rem;
font-family: monospace;
}
.hint {
font-size: 0.85rem;
color: #8b7355;
font-style: italic;
margin-top: 0.5rem;
}
strong {
color: #d4af37;
font-size: 1.5rem;
}
:global(body.dark) {
background: #0a0a0a;
}
:global(body.light) {
background: #f5f5f5;
}
</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. À quoi sert $effect ?
2. Comment effectuer un cleanup (nettoyage) dans $effect ?
Tâches pratiques dans le jeu
- Éditeur de code
Complète le code pour logger la valeur de count à chaque changement
- Clique dans l'ordre
Clique sur les bons cas d'usage de $effect dans l'ordre : localStorage, bibliothèques externes, logging