Cours Svelte et SvelteKit · Module 3: Réactivité et Runes

$effect - Effets de bord

4 min de lecture ·
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 load dans 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. 1. À quoi sert $effect ?

  2. 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

Articles utiles