Kurs Svelte i SvelteKit · Moduł 3: Reaktywność i runy
$effect - Efekty uboczne
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
loadw 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. Do czego służy $effect?
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