Le Runes di Svelte 5

LT
Luca Terribili
Autore

Nel primo tutorial abbiamo installato Svelte 5 con Vite e TypeScript e creato la nostra prima applicazione. Abbiamo visto come scrivere un componente .svelte, inserire markup HTML, utilizzare TypeScript e gestire un'interazione attraverso un pulsante.

Ma c'è una domanda a cui dobbiamo ancora rispondere: come fa Svelte a sapere che un dato è cambiato e che deve aggiornare l'interfaccia?

In JavaScript possiamo dichiarare una variabile con let, assegnarle un valore e modificarla quando vogliamo. Tuttavia, modificare una variabile non significa automaticamente aggiornare il contenuto della pagina. Il browser non osserva ogni variabile JavaScript alla ricerca di modifiche: serve un meccanismo che colleghi i dati all'interfaccia.

Svelte mette a disposizione un sistema di reattività che permette di creare questo collegamento. In Svelte 5 il meccanismo si basa principalmente sulle Runes, costrutti speciali riconosciuti dal compilatore. Attraverso di esse possiamo dichiarare lo stato reattivo, calcolare valori dipendenti da altri dati, eseguire operazioni quando cambiano determinate dipendenze e ricevere proprietà da altri componenti.

In questo tutorial studieremo $state, $derived, $effect e $props. Per ogni Rune vedremo a cosa serve, come si utilizza, quali problemi risolve e quali errori evitare. Alla fine realizzeremo una piccola applicazione che combina i concetti e ci permetterà di osservare il comportamento della reattività nella pratica.

Che cosa sono le Runes

Le Runes sono costrutti speciali di Svelte 5 che permettono di dichiarare esplicitamente determinati comportamenti reattivi. I loro nomi iniziano con il simbolo $, per esempio $state e $derived.

Non sono funzioni JavaScript ordinarie e non dobbiamo importarle da un pacchetto. Il compilatore Svelte riconosce questi costrutti e li interpreta secondo le regole del framework.

Consideriamo questo esempio:

<script lang="ts">
  let count = $state(0);
</script>

<p>Valore: {count}</p>

<button onclick={() => count++}>
  Incrementa
</button>

La dichiarazione let count = $state(0) crea uno stato reattivo inizializzato a zero. Quando premiamo il pulsante, count++ incrementa il valore e Svelte aggiorna il testo mostrato nella pagina.

Se avessimo utilizzato semplicemente let count = 0, nel normale modello di un componente Svelte 5 non avremmo dichiarato uno stato reattivo con le Runes. La modifica della variabile, da sola, non garantirebbe quindi l'aggiornamento dell'interfaccia.

Le Runes non sostituiscono JavaScript o TypeScript: si utilizzano all'interno del codice Svelte per indicare al compilatore come deve essere gestita la reattività. Il loro scopo è rendere esplicito il rapporto tra dati, dipendenze e aggiornamenti.

$state: dichiarare lo stato reattivo

La prima Rune da conoscere è $state. La utilizziamo quando vogliamo dichiarare un dato il cui cambiamento deve essere rilevato da Svelte.

Uno stato può rappresentare un numero, una stringa, un valore booleano, un array oppure un oggetto. Per esempio, in un'applicazione di gestione prodotti potremmo avere il numero di articoli nel carrello, il nome di un prodotto selezionato oppure lo stato di apertura di un menu.

La sintassi di base è semplice:

let count = $state(0);
let username = $state('Luca');
let isLoggedIn = $state(false);

In tutti e tre i casi dichiariamo variabili reattive, ma con tipi differenti. TypeScript può inferire i tipi iniziali: count è un numero, username è una stringa e isLoggedIn è un booleano.

Modificare lo stato

Una volta dichiarato lo stato, possiamo modificarlo attraverso le normali operazioni JavaScript:

<script lang="ts">
  let count = $state(0);

  function increment() {
    count++;
  }

  function decrement() {
    count--;
  }

  function reset() {
    count = 0;
  }
</script>

<h1>Contatore: {count}</h1>

<button onclick={decrement}>Decrementa</button>
<button onclick={increment}>Incrementa</button>
<button onclick={reset}>Azzera</button>

Non dobbiamo chiamare un metodo come setCount() né comunicare manualmente a Svelte che il valore è cambiato. Quando modifichiamo lo stato reattivo, Svelte rileva la modifica e aggiorna le parti interessate dell'interfaccia.

Questo esempio introduce un concetto fondamentale: la variabile contiene il dato, mentre $state permette a Svelte di gestirlo come stato reattivo.

Stato reattivo con oggetti e array

La reattività non si limita ai valori primitivi. Possiamo utilizzarla anche per oggetti e array:

<script lang="ts">
  let user = $state({
    name: 'Luca',
    age: 49
  });

  let products = $state<string[]>([
    'Tastiera',
    'Mouse'
  ]);
</script>

<p>Nome: {user.name}</p>
<p>Età: {user.age}</p>

<button onclick={() => user.age++}>
  Aumenta età
</button>

<button onclick={() => products.push('Monitor')}>
  Aggiungi prodotto
</button>

<ul>
  {#each products as product}
    <li>{product}</li>
  {/each}
</ul>

Per gli oggetti semplici e gli array, $state utilizza proxy reattivi profondi: questo significa che può rilevare anche modifiche alle proprietà e agli elementi, non soltanto una nuova assegnazione dell'intero oggetto o array.

Per esempio, user.age++ modifica una proprietà interna dell'oggetto, mentre products.push('Monitor') aggiunge un elemento all'array. In entrambi i casi Svelte può aggiornare l'interfaccia interessata.

Questa reattività profonda non si applica indistintamente a qualsiasi tipo di oggetto JavaScript. Per esempio, le istanze di classi personalizzate non diventano automaticamente proxy profondi. È quindi importante capire che $state rende reattivi i dati secondo le regole previste da Svelte, non trasforma qualsiasi oggetto in una struttura universalmente osservabile.

$derived: calcolare valori a partire dallo stato

La seconda Rune è $derived. La utilizziamo quando un valore può essere calcolato a partire da altri dati reattivi.

Immaginiamo di avere il prezzo di un prodotto e la quantità acquistata. Il totale è il risultato della moltiplicazione tra questi due valori. Potremmo memorizzare anche il totale in un'altra variabile, ma così avremmo tre dati da mantenere coerenti: prezzo, quantità e totale.

Con $derived possiamo evitare questa duplicazione.

<script lang="ts">
  let price = $state(15);
  let quantity = $state(2);

  let total = $derived(price * quantity);
</script>

<p>Prezzo unitario: {price} €</p>
<p>Quantità: {quantity}</p>
<p>Totale: {total} €</p>

<button onclick={() => quantity++}>
  Aggiungi un'unità
</button>

Il valore total viene calcolato a partire da price e quantity. Se uno dei due cambia, Svelte aggiorna il valore derivato e, di conseguenza, il totale visualizzato.

Non dobbiamo aggiornare manualmente total in ogni funzione che modifica il prezzo o la quantità. La dipendenza è espressa direttamente nella formula.

Perché non usare sempre $state?

Possiamo provare a risolvere lo stesso problema con tre stati distinti:

let price = $state(15);
let quantity = $state(2);
let total = $state(30);

In questo caso, quando cambia price, dovremmo ricordarci di aggiornare anche total. Lo stesso vale quando cambia quantity. Se dimenticassimo uno degli aggiornamenti, l'interfaccia mostrerebbe un totale non più coerente con i dati.

Con $derived, invece, il totale non rappresenta un'informazione indipendente: è il risultato di una formula. È preferibile esprimere questa relazione anziché duplicare lo stesso concetto nello stato.

La regola pratica è questa: utilizziamo $state per i dati che cambiano autonomamente e $derived per i valori che possiamo calcolare da altri dati.

Utilizzare $derived.by() per calcoli più articolati

Quando il calcolo richiede più istruzioni, possiamo utilizzare $derived.by():

<script lang="ts">
  let prices = $state([12, 25, 8]);

  let total = $derived.by(() => {
    let sum = 0;

    for (const price of prices) {
      sum += price;
    }

    return sum;
  });
</script>

<p>Totale: {total} €</p>

<button onclick={() => prices.push(10)}>
  Aggiungi 10 €
</button>

La funzione passata a $derived.by() restituisce il valore che vogliamo calcolare. Svelte rileva le dipendenze reattive utilizzate durante il calcolo e aggiorna il valore derivato quando tali dipendenze cambiano.

Come per $derived, il calcolo dovrebbe essere privo di effetti collaterali: serve a produrre un valore, non a modificare altri stati o a eseguire operazioni esterne.

$effect: reagire ai cambiamenti

La terza Rune è $effect. È quella che richiede maggiore attenzione, perché è facile utilizzarla anche quando non serve.

Con $effect possiamo eseguire un'operazione quando cambiano i dati reattivi da cui dipende. A differenza di $derived, che produce un valore, $effect serve per eseguire un'azione con un effetto esterno, per esempio interagire con un'API del browser o sincronizzare un dato con un sistema esterno.

Consideriamo questo esempio:

<script lang="ts">
  let count = $state(0);

  $effect(() => {
    document.title = `Contatore: ${count}`;
  });
</script>

<p>Valore: {count}</p>

<button onclick={() => count++}>
  Incrementa
</button>

Quando il componente è attivo nel browser, l'effetto legge count e aggiorna il titolo della scheda. Quando count cambia, Svelte riesegue l'effetto e aggiorna il titolo con il nuovo valore.

In questo caso $effect è adatto perché stiamo sincronizzando lo stato dell'applicazione con una risorsa esterna al normale rendering del componente: il titolo del documento.

Gli effetti vengono eseguiti nel browser, dopo che il componente è stato montato e dopo gli aggiornamenti del DOM pertinenti. Non vengono eseguiti durante il rendering lato server.

Come vengono individuate le dipendenze

Non dobbiamo specificare manualmente un elenco di dipendenze. Svelte rileva le letture di stato reattivo effettuate durante l'esecuzione sincrona dell'effetto.

Per esempio:

let count = $state(0);
let multiplier = $state(2);

$effect(() => {
  console.log(count * multiplier);
});

L'effetto legge sia count sia multiplier. Quando cambia uno dei due valori, l'effetto viene rieseguito.

Se invece una variabile reattiva non viene letta durante l'esecuzione dell'effetto, quella variabile non costituisce una dipendenza di quell'esecuzione. Le letture effettuate dentro una funzione asincrona, dopo un await, non vengono tracciate automaticamente come dipendenze dell'effetto.

Questa distinzione è utile quando le operazioni diventano più articolate, perché ci permette di capire quando e perché un effetto viene rieseguito.

Quando non utilizzare $effect

Un errore frequente consiste nell'utilizzare un effetto per calcolare un valore che potrebbe essere derivato direttamente dallo stato.

Per esempio, questo approccio è inutilmente complicato:

let price = $state(15);
let quantity = $state(2);
let total = $state(0);

$effect(() => {
  total = price * quantity;
});

Stiamo usando un effetto per mantenere sincronizzato un dato che dipende semplicemente da altri due dati. Per questo caso è preferibile:

let price = $state(15);
let quantity = $state(2);

let total = $derived(price * quantity);

La seconda soluzione esprime chiaramente la relazione tra prezzo, quantità e totale, senza introdurre un effetto collaterale e un ulteriore stato da mantenere.

Gli effetti non sono uno strumento generico per fare qualsiasi cosa quando cambia una variabile. Sono più appropriati quando dobbiamo sincronizzare lo stato con sistemi esterni, gestire integrazioni imperative o svolgere operazioni che non consistono semplicemente nel calcolare un altro valore.

$props: ricevere dati da un altro componente

La quarta Rune è $props. A differenza delle precedenti, non serve principalmente a dichiarare lo stato locale o a calcolare valori: serve a ricevere proprietà dal componente che utilizza il nostro componente.

Questo concetto sarà centrale nel prossimo tutorial sui componenti, ma è utile introdurlo già adesso per comprendere il modello completo di Svelte 5.

Supponiamo di creare un componente chiamato Greeting.svelte:

<script lang="ts">
  type Props = {
    name: string;
  };

  let { name }: Props = $props();
</script>

<p>Ciao, {name}!</p>

Il componente dichiara che si aspetta una proprietà name, di tipo stringa. La recupera attraverso $props() e la utilizza nel markup.

Un altro componente può utilizzarlo così:

<script lang="ts">
  import Greeting from './lib/Greeting.svelte';

  let username = $state('Luca');
</script>

<Greeting name={username} />

<button onclick={() => username = 'Marco'}>
  Cambia nome
</button>

Il componente padre possiede lo stato username e lo passa al componente figlio attraverso la prop name. Quando lo stato del padre cambia, il figlio riceve il nuovo valore e aggiorna il messaggio.

Questo esempio mostra come le Runes partecipino a ruoli differenti: $state gestisce il dato nel padre, mentre $props permette al figlio di riceverlo. La prop non è una copia statica del valore iniziale: nel normale utilizzo di Svelte, il valore ricevuto segue gli aggiornamenti del padre.

Le props non sono lo stato locale

È importante distinguere tra un dato posseduto dal componente e un dato ricevuto da un altro componente.

Nel nostro esempio username appartiene al padre. Il figlio riceve name e lo visualizza, ma non dovrebbe modificarlo direttamente. Se il figlio deve chiedere al padre di cambiare il valore, possiamo passargli una funzione callback come ulteriore prop.

Inoltre, $props() permette di definire valori predefiniti:

let { name = 'Ospite' }: { name?: string } = $props();

In questo caso, se il componente viene utilizzato senza fornire name, verrà usato il valore predefinito 'Ospite'. Il punto interrogativo nel tipo TypeScript indica che la proprietà può essere omessa.

La gestione delle props e delle callback sarà approfondita nel tutorial dedicato ai componenti. Per ora è sufficiente capire che $props rappresenta il confine attraverso il quale un componente riceve dati e funzioni dall'esterno.

Realizzare un'applicazione che utilizza le Runes

Adesso mettiamo insieme i concetti in un piccolo esempio. Apriamo src/App.svelte nel progetto svelte-study e sostituiamo il contenuto con questo codice:

<script lang="ts">
  let price = $state(12);
  let quantity = $state(1);
  let discount = $state(0);

  let subtotal = $derived(price * quantity);

  let total = $derived(
    subtotal * (1 - discount / 100)
  );

  $effect(() => {
    document.title = `Totale: ${total.toFixed(2)} €`;
  });

  function increaseQuantity() {
    quantity++;
  }

  function decreaseQuantity() {
    if (quantity > 1) {
      quantity--;
    }
  }

  function reset() {
    price = 12;
    quantity = 1;
    discount = 0;
  }
</script>

<svelte:head>
  <meta
    name="description"
    content="Esempio pratico delle Runes di Svelte 5"
  />
</svelte:head>

<main>
  <h1>Riepilogo ordine</h1>

  <section>
    <p>Prezzo unitario: {price.toFixed(2)} €</p>

    <div class="actions">
      <button onclick={() => price++}>
        Aumenta prezzo di 1 €
      </button>

      <button
        onclick={() => price = Math.max(1, price - 1)}
      >
        Riduci prezzo di 1 €
      </button>
    </div>

    <p>Quantità: {quantity}</p>

    <div class="actions">
      <button onclick={decreaseQuantity}>
        -
      </button>

      <button onclick={increaseQuantity}>
        +
      </button>
    </div>

    <label>
      Sconto:
      <select
        bind:value={discount}
      >
        <option value={0}>Nessuno</option>
        <option value={10}>10%</option>
        <option value={20}>20%</option>
      </select>
    </label>

    <hr />

    <p>Subtotale: {subtotal.toFixed(2)} €</p>
    <p>Sconto: {discount}%</p>

    <h2>Totale: {total.toFixed(2)} €</h2>

    <button onclick={reset}>
      Ripristina
    </button>
  </section>
</main>

<style>
  main {
    max-width: 680px;
    margin: 3rem auto;
    padding: 0 1.5rem;
    font-family: system-ui, sans-serif;
  }

  section {
    padding: 1.5rem;
    border: 1px solid #d1d5db;
    border-radius: 12px;
  }

  .actions {
    display: flex;
    gap: 0.75rem;
    margin-bottom: 1rem;
  }

  button,
  select {
    padding: 0.6rem 0.8rem;
    font: inherit;
  }

  button {
    cursor: pointer;
  }

  h2 {
    margin-top: 1.5rem;
  }
</style>

In questo esempio utilizziamo $state per dichiarare prezzo, quantità e sconto. Sono i dati che l'utente può modificare attraverso i pulsanti e il menu a tendina.

Con $derived calcoliamo il subtotale e il totale. Non aggiorniamo manualmente queste variabili quando cambiano prezzo, quantità o sconto: Svelte ricalcola i valori derivati in base alle dipendenze.

Con $effect sincronizziamo il totale con il titolo della scheda del browser. È un esempio concreto di utilizzo dell'effetto per interagire con una risorsa esterna al rendering. Il titolo viene aggiornato quando il valore derivato cambia.

Infine, bind:value={discount} collega il valore selezionato nel menu a tendina alla variabile reattiva discount. Questa sintassi è chiamata binding e sarà approfondita nel tutorial dedicato ai form.

Salviamo il file e avviamo l'applicazione, se il server non è già attivo:

npm run dev

Apriamo l'indirizzo indicato nel terminale e proviamo a modificare prezzo, quantità e sconto. Il subtotale e il totale cambieranno automaticamente. Anche il titolo della scheda del browser seguirà il totale aggiornato.

Verificare il codice

Se il progetto contiene lo script check, possiamo eseguire:

npm run check

Per verificare che l'applicazione venga compilata per la produzione, possiamo eseguire:

npm run build

Se uno dei comandi non è disponibile, controlliamo la sezione scripts del package.json. Gli script dipendono dal template e dalla configurazione del progetto.

Riepilogo: quale Rune utilizzare?

Le quattro Runes affrontate in questo tutorial rispondono a esigenze differenti.

  • $state dichiara un dato reattivo che può cambiare durante l'esecuzione dell'applicazione.
  • $derived calcola un valore a partire da altri dati reattivi, evitando di duplicare informazioni.
  • $effect esegue operazioni con effetti collaterali quando cambiano le dipendenze reattive che legge.
  • $props permette a un componente di ricevere dati e funzioni dal componente che lo utilizza.

Queste Runes non sono intercambiabili. Usare $effect per calcolare un semplice totale, per esempio, introduce complessità inutile; usare $state per memorizzare un valore che dovrebbe essere derivato può creare problemi di sincronizzazione.

Una buona regola è partire dalla domanda che dobbiamo risolvere: devo memorizzare un dato modificabile? Uso $state. Devo calcolare un risultato? Uso $derived. Devo sincronizzare un dato con qualcosa di esterno al rendering? Valuto $effect. Devo ricevere dati da un altro componente? Uso $props.

Conclusione

Abbiamo approfondito il modello reattivo di Svelte 5 e imparato a utilizzare le quattro Runes fondamentali per lo sviluppo dei nostri primi componenti. Ora sappiamo dichiarare lo stato, calcolare valori dipendenti, eseguire effetti e ricevere proprietà dall'esterno.

Il concetto più importante non è memorizzare la sintassi, ma capire la responsabilità di ciascun costrutto. La reattività collega i dati all'interfaccia; le Runes ci permettono di dichiarare come devono essere gestiti questi collegamenti.

Nel prossimo tutorial riprenderemo componenti, props ed eventi. Questa volta potremo affrontarli con le basi necessarie per capire perché un componente riceve dati, come questi si aggiornano e come può comunicare al componente padre che l'utente ha eseguito un'azione.