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
- Usa
$derivedinvece di$effectper calcolare valori derivati dallo stato - Evita effetti che modificano stato: crea cicli di aggiornamento difficili da debuggare
- Usa
$state.rawper dati grandi o immutabili dove la deep reactivity non serve - Estrai logica riutilizzabile in file
.svelte.tsper condividerla tra componenti - Minimizza gli effetti: meno
$effecthai, più prevedibile è il comportamento dell’app - Non dimenticare il cleanup nei
$effectche registrano listener o timer