$effect and the Lifecycle in Svelte 5: When to Use It (and When Not to)
If there is one Rune of Svelte 5 that is misunderstood and abused more than the others, it is $effect.
Coming from React, where useEffect and often the hammer for each nail, and natural
try to replicate that pattern in Svelte. But $effect has a different philosophy, a
narrower scope of use, and ignoring it leads directly to the framework's worst anti-patterns.
This guide precisely defines what it does $effect, like its tracking mechanism
automatic works internally, what is the golden rule for deciding whether to use it or not, and it provides
concrete examples of the main legitimate use cases: analytics, WebSockets, integration with libraries
third-party, and sync with localStorage.
The Golden Rule of $effect
Before writing $effect, ask yourself: “am I computing something from existing state?”
If the answer is yes, use $derived. $effect and for real side effects:
operations that interact with the world outside the UI (direct DOM, network, storage, timer,
third-party libraries).
How $effect Works: Automatic Tracking
$effect executes a function after the DOM has been updated. The feature
fundamental and that automatically tracks every $state and $derived it reads during
its execution and re-executes every time one of these dependencies changes.
It is not necessary to declare dependencies explicitly.
<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>
The tracking mechanism uses the same Proxy system as $state: during execution
of $effect, each access to a reactive value is logged. When any
of these values changes, the effect is marked as "dirty" and scheduled for re-play.
The Cleanup Function
$effect can return a cleanup function that runs before the
next re-execution of the effect and when the component is dismantled. It is essential for
avoid memory leaks with timers, subscriptions, and event listeners:
<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 changes, Svelte 5 cleans up (closes the old WS connection),
then re-executes the effect (opens a new connection with the new URL). And a clean pattern
which does not require manual lifecycle management.
Lifecycle: $effect vs onMount vs onDestroy
Svelte 5 maintains the lifecycle functions of Svelte 4 (onMount, onDestroy,
etc.) for backwards compatibility, but Runes offer a more uniform approach:
<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>
Legitimate Use Cases of $effect
Here are the main scenarios in which $effect and the correct choice:
1. Synchronization with 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 and 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. Integration with Third Party DOM Libraries
<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, Animations 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: Don't Use $effect for Calculations
The most common anti-pattern with $effect and use it to update state derived from something else
state. This creates inefficient and potentially infinite update cycles:
<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 and Infinite Loops
A classic mistake and edit within $effect one of the $states that the effect
same track, creating an infinite loop:
// CICLO INFINITO: count tracciato, count modificato
$effect(() => {
console.log(count); // traccia count
count++; // modifica count -> ri-esegue l'effect
});
Svelte 5 detects loops and throws an error in development. If you need to modify $state from an $effect,
use untrack() to read without tracing, or rethink the design.
$effect.root and $effect.pre
Svelte 5 offers two specialized variants of $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 Code with $effect
Test components with $effect requires attention: the effects are scheduled
asynchronously, so the tests have to wait for the update cycle to complete:
// 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');
});
Decision: $effect or $derived?
The simplified decision diagram:
- "Should I calculate a value from existing state?" → use
$derived - “Do I have to have an operation with side effects?” → use
$effect - "Should I update the DOM directly?" → use
$effect(or rather, an action Svelte) - "Should I make an API call when state changes?" → use
$effect - “Should I show transformed data in the UI?” → use
$derived
Conclusions and Next Steps
$effect and powerful but specialized: it is used to connect Svelte's responsive UI to the
external world — DOM, network, storage, third-party libraries. Use it for calculations that belong
a $derived introduces unnecessary complexity and potential bugs. The pattern to memorize:
if it produces a value to display in the UI, e $derived; if it interacts with something
outside the UI, e $effect.
The next article addresses SvelteKit in detail: how loads work functions for server-side data fetching, how SSR streaming reduces TTFB, and how forms actions handle mutations without client-side JavaScript.
Series: Svelte 5 and Frontend Compiler-Driven
- Article 1: Compiler-Driven Approach and Mental Model
- Article 2: $state and $derived — Universal Reactivity with Runes
- Article 3 (this): $effect and the Lifecycle — When to Use It (and When Not to)
- Article 4: SvelteKit SSR, Streaming and Load Functions
- Article 5: Transitions and Animations in Svelte 5
- Article 6: Accessibility in Svelte: Compiler Warnings and Best Practices
- Article 7: Global State Management: Context, Runes and Stores
- Article 8: Migrating from Svelte 4 to Svelte 5 — Practical Guide
- Article 9: Testing in Svelte 5: Vitest, Testing Library and Playwright







