$effect e il Ciclo di Vita in Svelte 5: Quando Usarlo (e Quando No)
Se c'e un Rune di Svelte 5 che viene frainteso e abusato piu degli altri, e $effect.
Arrivando da React, dove useEffect e spesso il martello per ogni chiodo, e naturale
cercare di replicare quel pattern in Svelte. Ma $effect ha una filosofia diversa, un
ambito d'uso piu ristretto, e ignorarlo porta direttamente ai peggiori anti-pattern del framework.
Questa guida definisce con precisione cosa fa $effect, come il suo meccanismo di tracking
automatico funziona internamente, qual e la regola d'oro per decidere se usarlo o meno, e fornisce
esempi concreti dei principali casi d'uso legittimi: analytics, WebSocket, integrazione con librerie
di terze parti, e sincronizzazione con localStorage.
La Regola d'Oro di $effect
Prima di scrivere $effect, chiediti: "sto calcolando qualcosa da stato esistente?"
Se la risposta e si, usa $derived. $effect e per side effects reali:
operazioni che interagiscono con il mondo esterno alla UI (DOM diretto, rete, storage, timer,
librerie di terze parti).
Come Funziona $effect: Tracking Automatico
$effect esegue una funzione dopo che il DOM e stato aggiornato. La caratteristica
fondamentale e che traccia automaticamente ogni $state e $derived che legge durante
la sua esecuzione e si ri-esegue ogni volta che una di queste dipendenze cambia.
Non e necessario dichiarare le dipendenze esplicitamente.
<script lang="ts">
let count = $state(0);
let name = $state('Federico');
// Questo effect dipende da ENTRAMBI count e name
// perche li legge entrambi durante l'esecuzione
$effect(() => {
console.log(`${name} ha un contatore di ${count}`);
// Si ri-esegue quando count O name cambiano
});
// Questo effect dipende SOLO da count
$effect(() => {
document.title = `Contatore: ${count}`;
// Si ri-esegue SOLO quando count cambia
});
</script>
Il meccanismo di tracking usa lo stesso sistema Proxy di $state: durante l'esecuzione
di $effect, ogni accesso a un valore reattivo viene registrato. Quando uno qualsiasi
di questi valori cambia, l'effect viene marcato come "dirty" e schedulato per ri-esecuzione.
La Funzione di Cleanup
$effect puo ritornare una funzione di cleanup che viene eseguita prima della
prossima ri-esecuzione dell'effect e quando il componente viene smontato. E essenziale per
evitare memory leak con timer, subscription, e event listener:
<script lang="ts">
let wsUrl = $state('wss://api.example.com/stream');
let messages = $state<string[]>([]);
$effect(() => {
// Effect traccia wsUrl: si ri-esegue se l'URL cambia
const ws = new WebSocket(wsUrl);
ws.addEventListener('message', (event) => {
messages.push(event.data);
});
ws.addEventListener('error', (error) => {
console.error('WebSocket error:', error);
});
// Cleanup: chiude la connessione prima della prossima esecuzione
// e quando il componente viene smontato
return () => {
ws.close();
console.log('WebSocket chiuso');
};
});
</script>
Se wsUrl cambia, Svelte 5 esegue il cleanup (chiude la vecchia connessione WS),
poi ri-esegue l'effect (apre una nuova connessione con il nuovo URL). E un pattern pulito
che non richiede gestione manuale del lifecycle.
Ciclo di Vita: $effect vs onMount vs onDestroy
Svelte 5 mantiene le lifecycle functions di Svelte 4 (onMount, onDestroy,
etc.) per retrocompatibilita, ma i Runes offrono un approccio piu uniforme:
<script lang="ts">
import { onMount, onDestroy } from 'svelte'; // Svelte 4 style
let data = $state(null);
// --- Svelte 4 pattern ---
onMount(async () => {
data = await fetchData();
});
// --- Svelte 5 equivalente con $effect ---
$effect(() => {
// $effect.root per effects che non dipendono da stato
// e si eseguono una sola volta (equivalente di onMount)
fetchData().then(result => {
data = result;
});
return () => {
// cleanup equivalente a onDestroy
};
});
// --- Svelte 5 per operazioni truly una-tantum ---
// Preferisci il blocco di codice nel componente
// o usa untrack() per evitare tracking accidentale
import { untrack } from 'svelte';
$effect(() => {
// untrack impedisce il tracking di initialValue
const initial = untrack(() => data);
console.log('Valore iniziale (non tracciato):', initial);
// count E tracciato perche e fuori da untrack
console.log('Count corrente:', count);
});
</script>
Casi d'Uso Legittimi di $effect
Ecco i principali scenari in cui $effect e la scelta corretta:
1. Sincronizzazione con localStorage
<script lang="ts">
// Leggi il valore iniziale da localStorage (fuori da $effect)
let theme = $state<'light' | 'dark'>(
(localStorage.getItem('theme') as 'light' | 'dark') ?? 'light'
);
// Sincronizza le modifiche a localStorage
$effect(() => {
localStorage.setItem('theme', theme);
document.documentElement.setAttribute('data-theme', theme);
});
</script>
<button onclick={() => theme = theme === 'light' ? 'dark' : 'light'}>
Toggle theme
</button>
2. Analytics e Tracking
<script lang="ts">
const { pageId, userId }: { pageId: string; userId?: string } = $props();
// Traccia la visualizzazione di pagina quando pageId cambia
$effect(() => {
// Sia pageId che userId sono tracciati
analytics.track('page_view', {
page: pageId,
user: userId ?? 'anonymous',
timestamp: new Date().toISOString()
});
});
</script>
3. Integrazione con Librerie DOM di Terze Parti
<script lang="ts">
import Chart from 'chart.js/auto';
let canvasEl: HTMLCanvasElement;
let chartData = $state({ labels: [], datasets: [] });
let chartInstance: Chart | null = null;
$effect(() => {
// Prima esecuzione: crea il chart
if (!chartInstance) {
chartInstance = new Chart(canvasEl, {
type: 'bar',
data: chartData
});
} else {
// Ri-esecuzioni: aggiorna i dati
chartInstance.data = chartData;
chartInstance.update();
}
return () => {
// Cleanup: distruggi il chart
chartInstance?.destroy();
chartInstance = null;
};
});
</script>
<canvas bind:this={canvasEl}></canvas>
4. Intersection Observer (Lazy Loading, Animazioni on Scroll)
<script lang="ts">
let element: HTMLElement;
let isVisible = $state(false);
let hasBeenVisible = $state(false);
$effect(() => {
const observer = new IntersectionObserver(
(entries) => {
isVisible = entries[0].isIntersecting;
if (isVisible) hasBeenVisible = true;
},
{ threshold: 0.1 }
);
observer.observe(element);
return () => observer.disconnect();
});
</script>
<div
bind:this={element}
class:visible={isVisible}
class:animated={hasBeenVisible}
>
Contenuto che appare con animazione
</div>
Anti-Pattern: Non Usare $effect per Calcoli
L'anti-pattern piu comune con $effect e usarlo per aggiornare stato derivato da altro
stato. Questo crea cicli di aggiornamento inefficienti e potenzialmente infiniti:
<script lang="ts">
let items = $state([1, 2, 3, 4, 5]);
let total = $state(0); // SBAGLIATO: non dovrebbe essere $state
// ANTI-PATTERN: usare $effect per calcolare un valore derivato
$effect(() => {
total = items.reduce((s, i) => s + i, 0);
// Questo crea: items cambia -> effect corre -> total cambia ->
// potenziali altri effects si ri-eseguono inutilmente
});
// CORRETTO: usare $derived
const totalCorrect = $derived(items.reduce((s, i) => s + i, 0));
</script>
<script lang="ts">
let searchQuery = $state('');
let filteredUsers = $state([]); // SBAGLIATO
// ANTI-PATTERN: filtrare con $effect
$effect(() => {
filteredUsers = users.filter(u =>
u.name.toLowerCase().includes(searchQuery.toLowerCase())
);
});
// CORRETTO: $derived
const filteredUsersCorrect = $derived(
users.filter(u =>
u.name.toLowerCase().includes(searchQuery.toLowerCase())
)
);
</script>
$effect e i Cicli Infiniti
Un errore classico e modificare all'interno di $effect uno degli $state che l'effect
stesso traccia, creando un ciclo infinito:
// CICLO INFINITO: count tracciato, count modificato
$effect(() => {
console.log(count); // traccia count
count++; // modifica count -> ri-esegue l'effect
});
Svelte 5 detecta i cicli e lancia un errore in development. Se devi modificare $state da un $effect,
usa untrack() per leggere senza tracciare, oppure ripensa il design.
$effect.root e $effect.pre
Svelte 5 offre due varianti specializzate di $effect:
<script lang="ts">
// $effect.pre: esegue PRIMA dell'aggiornamento del DOM
// Utile per leggere il DOM prima che venga modificato
// (es: salvare la posizione di scroll prima di un aggiornamento)
$effect.pre(() => {
const scrollPos = window.scrollY;
return () => {
// Ripristina scroll dopo l'aggiornamento del DOM
window.scrollTo(0, scrollPos);
};
});
// $effect.root: crea un effetto che non e legato al ciclo di vita
// del componente, utile fuori dai componenti
const cleanup = $effect.root(() => {
$effect(() => {
// Effect che persiste oltre la distruzione del componente
// (utile per singleton e stores globali)
});
return () => { /* cleanup manuale */ };
});
</script>
Testing di Codice con $effect
Testare componenti con $effect richiede attenzione: gli effects vengono schedulati
asincronamente, quindi i test devono aspettare che il ciclo di aggiornamento completi:
// test/MyComponent.test.ts
import { render } from '@testing-library/svelte';
import { tick } from 'svelte';
import MyComponent from './MyComponent.svelte';
test('$effect sincronizza con localStorage', async () => {
const { getByRole } = render(MyComponent);
const button = getByRole('button', { name: 'Toggle theme' });
button.click();
// Aspetta che il ciclo di aggiornamento Svelte completi
await tick();
expect(localStorage.getItem('theme')).toBe('dark');
});
Decisione: $effect o $derived?
Il diagramma decisionale semplificato:
- "Devo calcolare un valore da stato esistente?" → usa
$derived - "Devo eseguire un'operazione con effetti collaterali?" → usa
$effect - "Devo aggiornare il DOM direttamente?" → usa
$effect(o meglio, un action Svelte) - "Devo fare una chiamata API quando stato cambia?" → usa
$effect - "Devo mostrare dati trasformati nella UI?" → usa
$derived
Conclusioni e Prossimi Passi
$effect e potente ma specializzato: serve per collegare la UI reattiva di Svelte al
mondo esterno — DOM, rete, storage, librerie di terze parti. Usarlo per calcoli che appartengono
a $derived introduce complessita inutile e potenziali bug. Il pattern da memorizzare:
se produce un valore da mostrare nella UI, e $derived; se interagisce con qualcosa
fuori dalla UI, e $effect.
Il prossimo articolo affronta SvelteKit nel dettaglio: come funzionano le load functions per il data fetching server-side, come lo streaming SSR riduce il TTFB, e come le form actions gestiscono le mutazioni senza JavaScript lato client.
Serie: Svelte 5 e Frontend Compiler-Driven
- Articolo 1: Approccio Compiler-Driven e Modello Mentale
- Articolo 2: $state e $derived — Reattivita Universale con i Runes
- Articolo 3 (questo): $effect e il Ciclo di Vita — Quando Usarlo (e Quando No)
- Articolo 4: SvelteKit SSR, Streaming e Load Functions
- Articolo 5: Transizioni e Animazioni in Svelte 5
- Articolo 6: Accessibilita in Svelte: Compiler Warnings e Best Practice
- Articolo 7: State Management Globale: Context, Runes e Stores
- Articolo 8: Migrare da Svelte 4 a Svelte 5 — Guida Pratica
- Articolo 9: Testing in Svelte 5: Vitest, Testing Library e Playwright







