Kurs Svelte i SvelteKit · Moduł 3: Reaktywność i runy

$effect - Efekty uboczne

4 min czytania ·
W tej lekcji9

$effect to runa, która wykonuje kod w reakcji na zmiany. Używaj jej do synchronizacji z zewnętrznym światem.

Podstawowe użycie

<script>
  let count = $state(0);

  // Wykonuje się przy każdej zmianie count
  $effect(() => {
    console.log('Licznik:', count);
  });
</script>

<button onclick={() => count++}>
  Zwiększ ({count})
</button>

Kiedy używać $effect?

Dobre przypadki użycia:

  • Logowanie/debugging
  • Synchronizacja z localStorage
  • Integracja z zewnętrznymi bibliotekami
  • Nasłuchiwanie na zdarzenia
  • Timery i animacje

Unikaj:

  • Aktualizacji innych stanów (użyj $derived)
  • Fetchowania danych (użyj load w SvelteKit)

Przykład: localStorage

<script>
  // Wczytaj zapisany motyw przy starcie (albo użyj domyślnego)
  let theme = $state(localStorage.getItem('theme') ?? 'dark');

  // Zapisz do localStorage przy każdej zmianie
  $effect(() => {
    localStorage.setItem('theme', theme);
    document.body.className = theme;
  });
</script>

<select bind:value={theme}>
  <option value="dark">Ciemny (Noc Północy)</option>
  <option value="light">Jasny (Dzień Słońca)</option>
</select>

Zapisany motyw czytamy od razu w wartości początkowej $state, a efekt tylko zapisuje zmiany. Nie dziel tego na dwa efekty - zapisujący i wczytujący - bo zapis wykona się pierwszy i nadpisze zapamiętany motyw. W SvelteKit komponent renderuje się też na serwerze, gdzie nie ma localStorage - tam sięgaj po niego dopiero w przeglądarce, np. w onMount (Walhalla, moduł 7).

Cleanup - Sprzątanie

$effect może zwrócić funkcję czyszczącą:

<script>
  let isRunning = $state(false);
  let seconds = $state(0);

  $effect(() => {
    if (!isRunning) return;

    // Ustaw interval
    const interval = setInterval(() => {
      seconds += 1;
    }, 1000);

    // Cleanup - wyczyść przy następnym wywołaniu
    return () => {
      clearInterval(interval);
    };
  });
</script>

<p>Czas: {seconds}s</p>
<button onclick={() => isRunning = !isRunning}>
  {isRunning ? 'Stop' : 'Start'}
</button>

$effect.pre - Przed aktualizacją DOM

<script>
  let messages = $state([]);

  // Wykonuje się PRZED aktualizacją DOM
  $effect.pre(() => {
    console.log('Przed renderem, wiadomości:', messages.length);
  });
</script>

Zależności efektu

$effect automatycznie śledzi zależności:

<script>
  let a = $state(1);
  let b = $state(2);
  let c = $state(3);

  $effect(() => {
    // Ten efekt uruchomi się gdy zmieni się a lub b
    // NIE uruchomi się gdy zmieni się c
    console.log('a + b =', a + b);
  });
</script>

untrack - odczyt bez śledzenia

Czasem efekt musi odczytać stan, ale nie powinien od niego zależeć. Typowy przykład to dziennik zmian: efekt reaguje na count i dopisuje wpis do tablicy logs. Gdyby czytał logs w zwykły sposób, uruchamiałby sam siebie w kółko, a Svelte przerwałby to błędem effect_update_depth_exceeded. Funkcja untrack czyta stan bez śledzenia:

<script>
  import { untrack } from 'svelte';

  let count = $state(0);
  let logs = $state([]);

  $effect(() => {
    const entry = `Licznik: ${count}`; // zależność: count
    untrack(() => {
      logs = [...logs, entry].slice(-5); // logs czytane bez śledzenia
    });
  });
</script>

<button onclick={() => count++}>+1</button>
<ul>
  {#each logs as log}
    <li>{log}</li>
  {/each}
</ul>

Pamiętaj jednak o zasadzie: jeśli wartość da się policzyć z innych, użyj $derived, a nie efektu.

$inspect - podgląd w konsoli

Do debugowania zamiast efektu z console.log możesz użyć runy $inspect. Wypisuje wartość w konsoli przy starcie i po każdej zmianie, także zmianie głęboko w obiekcie. Działa tylko w trybie deweloperskim - w zbudowanej aplikacji znika:

<script>
  let warrior = $state({ name: 'Ragnar', health: 100 });

  $inspect(warrior); // loguje cały obiekt po każdej zmianie
</script>

<button onclick={() => warrior.health -= 10}>
  {warrior.name}: {warrior.health} HP
</button>

Przykład: Integracja z zewnętrzną biblioteką

<script>
  import Chart from 'chart.js/auto';

  let loot = $state([10, 20, 30, 40]);
  let canvas;

  $effect(() => {
    // Efekt czyta loot, więc uruchomi się ponownie po każdej zmianie
    const chart = new Chart(canvas, {
      type: 'bar',
      data: {
        labels: ['Ragnar', 'Bjorn', 'Ivar', 'Ubbe'],
        datasets: [{ label: 'Łupy', data: $state.snapshot(loot) }]
      }
    });

    // Cleanup - zniszcz stary wykres, zanim powstanie nowy
    return () => chart.destroy();
  });
</script>

<canvas bind:this={canvas}></canvas>
<button onclick={() => loot[0] += 10}>Ragnar zdobywa łup</button>

Efekt rusza po zamontowaniu komponentu, więc canvas jest już gotowy. $state.snapshot(loot) daje bibliotece zwykłą kopię tablicy zamiast reaktywnego proxy - zewnętrzne biblioteki wolą zwykłe dane. Każda zmiana łupów niszczy stary wykres w funkcji czyszczącej i rysuje nowy.

Kod do tej lekcji: 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>

Hasła z dokumentacji w tej lekcji

Widzisz błąd w tej lekcji?

Sprawdź się

Odpowiedz na pytania z tej lekcji. Wybierz odpowiedź, a od razu zobaczysz, czy jest poprawna.

  1. 1. Do czego służy $effect?

  2. 2. Jak wykonać cleanup (sprzątanie) w $effect?

Zadania praktyczne w grze

  • Edytor kodu

    Uzupełnij kod, aby logować wartość count przy każdej zmianie

  • Klikanie w kolejności

    Kliknij dobre przypadki użycia $effect w kolejności: localStorage, zewnętrzne biblioteki, logowanie

Przydatne artykuły