00
:
00
:
00
:
00
Corso SEO AI - Usa SEOEMAIL al checkout per il 30% di sconto

Reattività e Runes

Il Sistema di Reattività di Svelte

La reattività è il cuore di Svelte. Quando lo stato cambia, l’interfaccia si aggiorna automaticamente. Svelte 5 introduce le runes, un sistema di reattività basato su segnali (signals) che sostituisce il modello “magico” di Svelte 4 con primitive esplicite e componibili.

$state: Stato Reattivo

$state crea una variabile reattiva. Quando il suo valore cambia, tutti i punti del markup che la utilizzano vengono aggiornati.

Valori primitivi

<script>
  let contatore = $state(0);
  let nome = $state('Mario');
  let attivo = $state(false);
</script>

<p>Contatore: {contatore}</p>
<p>Nome: {nome}</p>
<p>Stato: {attivo ? 'Attivo' : 'Inattivo'}</p>

<button onclick={() => contatore++}>+1</button>
<button onclick={() => attivo = !attivo}>Toggle</button>

Oggetti e array

$state rende gli oggetti e gli array profondamente reattivi (deep reactivity). Le mutazioni vengono tracciate automaticamente:

<script>
  let utente = $state({
    nome: 'Luca',
    eta: 25,
    indirizzo: {
      citta: 'Roma',
      cap: '00100'
    }
  });

  let frutti = $state(['Mela', 'Banana', 'Arancia']);

  function aggiornaCitta() {
    // La mutazione diretta funziona! Svelte traccia il cambiamento
    utente.indirizzo.citta = 'Milano';
  }

  function aggiungiPera() {
    // push() funziona direttamente in Svelte 5
    frutti.push('Pera');
  }

  function rimuoviPrimo() {
    frutti.shift();
  }
</script>

<p>{utente.nome} vive a {utente.indirizzo.citta}</p>
<ul>
  {#each frutti as frutto}
    <li>{frutto}</li>
  {/each}
</ul>

Differenza fondamentale con Svelte 4

<!-- Svelte 4: serviva riassegnare per triggerare la reattività -->
<script>
  let lista = [1, 2, 3];

  function aggiungi() {
    lista.push(4);
    lista = lista; // Hack necessario! Oppure: lista = [...lista, 4];
  }

  let obj = { a: 1 };
  function modifica() {
    obj.a = 2;
    obj = obj; // Hack necessario!
  }
</script>

<!-- Svelte 5: le mutazioni sono tracciate nativamente -->
<script>
  let lista = $state([1, 2, 3]);
  let obj = $state({ a: 1 });

  function aggiungi() {
    lista.push(4); // Funziona perfettamente
  }

  function modifica() {
    obj.a = 2; // Funziona perfettamente
  }
</script>

$state con classi

class Contatore {
  valore = $state(0);

  incrementa() {
    this.valore++;
  }

  decrementa() {
    this.valore--;
  }

  reset() {
    this.valore = 0;
  }
}
<script>
  const contatore = new Contatore();
</script>

<p>Valore: {contatore.valore}</p>
<button onclick={() => contatore.incrementa()}>+</button>
<button onclick={() => contatore.decrementa()}>-</button>
<button onclick={() => contatore.reset()}>Reset</button>

$derived: Valori Derivati

$derived crea un valore calcolato che si aggiorna automaticamente quando le sue dipendenze cambiano. Sostituisce le dichiarazioni reattive $: di Svelte 4.

Uso base

<script>
  let prezzo = $state(100);
  let quantita = $state(2);
  let sconto = $state(10);

  // Si ricalcola automaticamente quando prezzo, quantità o sconto cambiano
  let subtotale = $derived(prezzo * quantita);
  let importoSconto = $derived(subtotale * sconto / 100);
  let totale = $derived(subtotale - importoSconto);
</script>

<p>Subtotale: {subtotale} EUR</p>
<p>Sconto ({sconto}%): -{importoSconto} EUR</p>
<p>Totale: {totale} EUR</p>

<input type="number" bind:value={quantita} min="1" />
<input type="range" bind:value={sconto} min="0" max="50" />

$derived con logica complessa

Per derivazioni che richiedono più di una singola espressione, usa $derived.by():

<script>
  let items = $state([
    { nome: 'Mela', prezzo: 1.5, quantita: 3 },
    { nome: 'Banana', prezzo: 0.8, quantita: 5 },
    { nome: 'Arancia', prezzo: 2.0, quantita: 2 }
  ]);

  let filtro = $state('');

  // Derivazione con logica complessa
  let itemsFiltrati = $derived.by(() => {
    if (!filtro) return items;
    const termine = filtro.toLowerCase();
    return items.filter(item =>
      item.nome.toLowerCase().includes(termine)
    );
  });

  let totaleCarrello = $derived.by(() => {
    return items.reduce((acc, item) => {
      return acc + item.prezzo * item.quantita;
    }, 0);
  });

  let riepilogo = $derived.by(() => {
    const numItems = items.length;
    const totale = totaleCarrello;
    if (numItems === 0) return 'Carrello vuoto';
    return `${numItems} prodotti per un totale di ${totale.toFixed(2)} EUR`;
  });
</script>

<input bind:value={filtro} placeholder="Cerca prodotto..." />
<p>{riepilogo}</p>

{#each itemsFiltrati as item}
  <p>{item.nome}: {item.prezzo} EUR x {item.quantita}</p>
{/each}

Confronto con Svelte 4

<!-- Svelte 4 -->
<script>
  let a = 1;
  let b = 2;
  $: somma = a + b;                        // derivato semplice
  $: prodotto = a * b;                      // derivato semplice
  $: risultato = somma > 5 ? 'alto' : 'basso'; // derivato da derivato
</script>

<!-- Svelte 5 -->
<script>
  let a = $state(1);
  let b = $state(2);
  let somma = $derived(a + b);
  let prodotto = $derived(a * b);
  let risultato = $derived(somma > 5 ? 'alto' : 'basso');
</script>

$effect: Effetti Collaterali

$effect esegue codice quando le sue dipendenze reattive cambiano. Le dipendenze vengono tracciate automaticamente. Sostituisce gli statement $: con side effect di Svelte 4.

Uso base

<script>
  let contatore = $state(0);
  let tema = $state('chiaro');

  // Eseguito ad ogni cambio di contatore
  $effect(() => {
    console.log(`Contatore aggiornato: ${contatore}`);
  });

  // Eseguito ad ogni cambio di tema
  $effect(() => {
    document.body.classList.toggle('dark', tema === 'scuro');
  });
</script>

Cleanup degli effetti

La funzione restituita da $effect viene chiamata prima della prossima esecuzione e quando il componente viene distrutto:

<script>
  let larghezza = $state(window.innerWidth);
  let intervallo = $state(1000);
  let tick = $state(0);

  // Effetto con cleanup: gestione event listener
  $effect(() => {
    function handleResize() {
      larghezza = window.innerWidth;
    }
    window.addEventListener('resize', handleResize);

    // Cleanup: rimuovi il listener
    return () => {
      window.removeEventListener('resize', handleResize);
    };
  });

  // Effetto con cleanup: intervallo che dipende da una variabile
  $effect(() => {
    const id = setInterval(() => {
      tick++;
    }, intervallo);

    // Quando `intervallo` cambia, il vecchio setInterval viene pulito
    return () => clearInterval(id);
  });
</script>

<p>Larghezza finestra: {larghezza}px</p>
<p>Tick: {tick} (ogni {intervallo}ms)</p>
<input type="range" bind:value={intervallo} min="100" max="5000" step="100" />

$effect.pre

$effect.pre viene eseguito prima dell’aggiornamento del DOM, utile per casi come il mantenimento della posizione dello scroll:

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

  // Viene eseguito prima che il DOM si aggiorni
  $effect.pre(() => {
    // Salva la posizione di scroll prima che nuovi messaggi vengano aggiunti
    if (container) {
      const isScrolledToBottom =
        container.scrollHeight - container.scrollTop === container.clientHeight;
      if (isScrolledToBottom) {
        // Dopo l'aggiornamento DOM, scorri in fondo
        tick().then(() => {
          container.scrollTop = container.scrollHeight;
        });
      }
    }
    // Accesso a messaggi per creare la dipendenza
    messaggi.length;
  });
</script>

Quando NON usare $effect

$effect non dovrebbe essere usato per sincronizzare stato. Usa $derived invece:

<script>
  let nome = $state('Mario');
  let cognome = $state('Rossi');

  // SBAGLIATO: non usare $effect per derivare stato
  let nomeCompleto = $state('');
  $effect(() => {
    nomeCompleto = `${nome} ${cognome}`;  // Anti-pattern!
  });

  // CORRETTO: usa $derived
  let nomeCompleto2 = $derived(`${nome} ${cognome}`);
</script>

$state.raw: Stato Non-Profondo

Per oggetti grandi o immutabili dove non serve la deep reactivity:

<script>
  // Non traccia le mutazioni interne - solo la riassegnazione
  let datiGrandi = $state.raw({
    items: new Array(10000).fill(null).map((_, i) => ({ id: i, valore: Math.random() }))
  });

  function aggiorna() {
    // Devi riassegnare l'intero oggetto (come Svelte 4)
    datiGrandi = {
      ...datiGrandi,
      items: datiGrandi.items.map(item => ({
        ...item,
        valore: Math.random()
      }))
    };
  }
</script>

$state.raw e’ utile per:

  • Grandi dataset dove la deep reactivity ha un costo
  • Oggetti provenienti da librerie esterne (mappe, set specializzati)
  • Dati immutabili per design

$state.snapshot

Per ottenere una copia plain (non-proxy) dello stato, utile per logging o serializzazione:

<script>
  let dati = $state({ nome: 'Test', items: [1, 2, 3] });

  function salva() {
    // $state.snapshot restituisce una copia senza proxy
    const snapshot = $state.snapshot(dati);
    console.log(snapshot); // Oggetto plain, non Proxy
    localStorage.setItem('dati', JSON.stringify(snapshot));
  }
</script>

Reattività Fuori dai Componenti

Le runes funzionano anche in file .svelte.ts o .svelte.js, permettendo di estrarre logica reattiva riutilizzabile:

// src/lib/contatore.svelte.ts
export function creaContatore(iniziale = 0) {
  let valore = $state(iniziale);
  let doppio = $derived(valore * 2);

  return {
    get valore() { return valore; },
    get doppio() { return doppio; },
    incrementa() { valore++; },
    decrementa() { valore--; },
    reset() { valore = iniziale; }
  };
}
<script>
  import { creaContatore } from '$lib/contatore.svelte';

  const contatore = creaContatore(10);
</script>

<p>Valore: {contatore.valore} (Doppio: {contatore.doppio})</p>
<button onclick={() => contatore.incrementa()}>+</button>
<button onclick={() => contatore.decrementa()}>-</button>

Fine-Grained Reactivity

Svelte 5 offre reattività granulare: solo le parti del DOM che dipendono dallo stato cambiato vengono aggiornate.

<script>
  let utente = $state({
    nome: 'Mario',
    eta: 30,
    hobby: ['calcio', 'cucina']
  });
</script>

<!-- Quando cambi utente.nome, SOLO questo paragrafo viene aggiornato -->
<p>Nome: {utente.nome}</p>

<!-- Quando cambi utente.eta, SOLO questo paragrafo viene aggiornato -->
<p>Età: {utente.eta}</p>

<!-- L'array hobby ha la sua tracciatura indipendente -->
<ul>
  {#each utente.hobby as h}
    <li>{h}</li>
  {/each}
</ul>

Best Practice

  1. Usa $derived invece di $effect per calcolare valori derivati dallo stato
  2. Evita effetti che modificano stato: crea cicli di aggiornamento difficili da debuggare
  3. Usa $state.raw per dati grandi o immutabili dove la deep reactivity non serve
  4. Estrai logica riutilizzabile in file .svelte.ts per condividerla tra componenti
  5. Minimizza gli effetti: meno $effect hai, più prevedibile è il comportamento dell’app
  6. Non dimenticare il cleanup nei $effect che registrano listener o timer