Salta al contenuto principale

03 - $effect e il Ciclo di Vita: Quando Usarlo (e Quando No)

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

Cosa ne pensi di questo articolo?

03 - $effect e il Ciclo di Vita: Quando Usarlo (e Quando No)
Svelte 5Foto:Ruben BoekelooPexels

In Breve

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

$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

Prova Questi Strumenti

Strumenti gratuiti correlati a questo argomento.

Commenti

Caricamento commenti...

Accedi per commentare

Discuti questo articolo nella community

Unisciti al gruppo Telegram per discutere con altri sviluppatori, fare domande e condividere le tue esperienze.

Ti è piaciuto questo articolo?

Esplora altri contenuti sul blog o scopri i miei progetti