Componenti, props ed eventi in Svelte 5 con TypeScript

LT
Luca Terribili
Autore

Nel primo tutorial abbiamo creato il nostro progetto Svelte utilizzando Vite e TypeScript. Abbiamo visto come avviare il server di sviluppo e come modificare il componente principale dell'applicazione.

Nel secondo tutorial abbiamo approfondito la reattività di Svelte 5, imparando a utilizzare le Runes $state, $derived e $effect. Questi strumenti ci permettono di gestire i dati, calcolare valori derivati e reagire a determinati cambiamenti.

Adesso dobbiamo affrontare un problema che si presenta in qualsiasi applicazione frontend destinata a crescere: come organizzare il codice senza concentrare tutta l'interfaccia in un unico file?

Immaginiamo di sviluppare un'applicazione per la gestione di prodotti. Potremmo avere un componente per mostrare un prodotto, uno per il carrello, uno per il riepilogo dell'ordine e altri per gestire le diverse funzionalità. Scrivere tutto dentro App.svelte renderebbe rapidamente il progetto difficile da mantenere.

Svelte risolve questo problema attraverso i componenti. Ogni componente può contenere markup, logica TypeScript e stili CSS, ed essere utilizzato all'interno di altri componenti. Per comunicare tra loro utilizziamo principalmente le props, che permettono di passare dati, e le funzioni callback, che consentono a un componente figlio di notificare al componente padre che è successo qualcosa.

In questo tutorial costruiremo un componente contatore riutilizzabile. Il componente riceverà un titolo, un valore e alcune funzioni dal componente principale. In questo modo vedremo concretamente come separare la visualizzazione dalla gestione dello stato.

Che cosa sono i componenti Svelte

Un componente Svelte è un file con estensione .svelte. Al suo interno possiamo definire tre sezioni principali: uno script per la logica, il markup HTML per l'interfaccia e uno stile CSS per la presentazione.

Consideriamo un semplice esempio:

<script lang="ts">
  let name = $state('Luca');
</script>

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

<style>
  h1 {
    color: steelblue;
  }
</style>

Lo script contiene una variabile reattiva, il markup visualizza il messaggio e la sezione CSS definisce il colore del titolo. Quando il valore di name cambia, Svelte aggiorna automaticamente il testo visualizzato.

La cosa importante è che un componente non deve necessariamente contenere tutte e tre le sezioni. Un componente che visualizza soltanto del testo può avere esclusivamente markup, mentre uno che gestisce una funzionalità più articolata può combinare TypeScript, HTML e CSS.

Possiamo utilizzare un componente all'interno di un altro importandolo come un normale modulo. Per esempio, App.svelte può importare Counter.svelte e inserirlo nel proprio markup. Il componente importato diventa così una parte dell'interfaccia dell'applicazione.

Questa organizzazione consente di dividere il progetto in unità più piccole, ciascuna con una responsabilità precisa. È lo stesso principio che utilizzeremo quando svilupperemo interfacce più complesse collegate al nostro backend Node.js.

Creare il primo componente riutilizzabile

Apriamo il progetto svelte-study creato nel primo tutorial. All'interno della cartella src, creiamo una nuova directory chiamata lib, se non esiste già.

La struttura sarà questa:

svelte-study/
└── src/
    ├── lib/
    │   └── Counter.svelte
    ├── App.svelte
    ├── app.css
    └── main.ts

La cartella lib è una convenzione comune nei progetti Svelte per raccogliere componenti e moduli riutilizzabili. Non è obbligatoria, ma aiuta a mantenere ordinata l'applicazione.

Creiamo il file src/lib/Counter.svelte e inseriamo il seguente codice:

<script lang="ts">
  type Props = {
    title: string;
    value: number;
    onIncrement: () => void;
    onReset: () => void;
  };

  let {
    title,
    value,
    onIncrement,
    onReset
  }: Props = $props();
</script>

<section>
  <h2>{title}</h2>

  <p>Valore attuale: {value}</p>

  <div class="actions">
    <button onclick={onIncrement}>
      Incrementa
    </button>

    <button onclick={onReset}>
      Azzera
    </button>
  </div>
</section>

<style>
  section {
    padding: 1.5rem;
    border: 1px solid #d1d5db;
    border-radius: 10px;
  }

  h2 {
    margin-top: 0;
  }

  p {
    font-size: 1.25rem;
    font-weight: 600;
  }

  .actions {
    display: flex;
    gap: 0.75rem;
  }

  button {
    padding: 0.6rem 1rem;
    border: 0;
    border-radius: 6px;
    cursor: pointer;
    font: inherit;
  }
</style>

Questo componente visualizza un titolo, un valore numerico e due pulsanti. Non decide autonomamente quale valore conservare: riceve il valore dall'esterno e invoca le funzioni ricevute quando l'utente preme i pulsanti.

Questa scelta è intenzionale. Vogliamo che il componente si occupi della visualizzazione e delle interazioni, lasciando al componente padre la responsabilità di gestire lo stato.

Passare dati attraverso le props

Le props sono i dati che un componente riceve dal componente che lo utilizza. In Svelte 5 le dichiariamo principalmente attraverso la Rune $props().

Nel nostro esempio abbiamo definito un tipo TypeScript chiamato Props:

type Props = {
  title: string;
  value: number;
  onIncrement: () => void;
  onReset: () => void;
};

Questo tipo descrive le informazioni che il componente si aspetta di ricevere. title è una stringa, value è un numero e le altre due proprietà sono funzioni che non ricevono argomenti e non restituiscono un valore.

Successivamente utilizziamo:

let {
  title,
  value,
  onIncrement,
  onReset
}: Props = $props();

Con questa istruzione recuperiamo le props ricevute e le assegniamo a variabili locali. TypeScript ci aiuta a individuare errori, per esempio se proviamo a passare una stringa dove è richiesto un numero.

Le props sono particolarmente utili quando vogliamo riutilizzare lo stesso componente in contesti differenti. Un componente Counter può visualizzare un contatore per i prodotti, uno per gli articoli completati oppure uno per le notifiche, semplicemente ricevendo valori diversi.

È importante distinguere le props dallo stato locale. Nel nostro caso value arriva dal padre: il componente figlio non deve modificarlo direttamente. Quando il padre cambia il valore e passa la nuova prop, il componente figlio visualizza automaticamente il dato aggiornato.

Comunicare dal componente figlio al componente padre

Finora abbiamo visto come trasferire dati dal padre al figlio. Ma cosa succede quando l'utente preme il pulsante «Incrementa» all'interno del componente figlio?

Il figlio deve comunicare al padre che è stata richiesta un'operazione. In Svelte 5 possiamo farlo passando una funzione callback come prop.

Nel nostro componente abbiamo già scritto:

<button onclick={onIncrement}>
  Incrementa
</button>

L'evento onclick richiama la funzione ricevuta dal componente padre. Il figlio non ha bisogno di conoscere come il padre gestisce il valore: deve soltanto invocare la funzione quando l'utente interagisce con il pulsante.

Lo stesso principio vale per il pulsante di azzeramento:

<button onclick={onReset}>
  Azzera
</button>

Questa è la sintassi moderna di Svelte 5 per gestire gli eventi DOM. Nei tutorial o nei progetti meno recenti potresti trovare on:click, ma qui utilizzeremo onclick.

La differenza architetturale è importante: il componente figlio gestisce l'interazione, mentre il padre decide cosa fare quando l'interazione si verifica. Questo rende il componente riutilizzabile e riduce la dipendenza tra le diverse parti dell'applicazione.

Utilizzare il componente in App.svelte

Adesso colleghiamo il componente al resto dell'applicazione. Apriamo src/App.svelte e sostituiamo il contenuto con il seguente codice:

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

  let firstCount = $state(0);
  let secondCount = $state(10);
</script>

<svelte:head>
  <title>Componenti Svelte 5</title>
  <meta
    name="description"
    content="Esempio di componenti, props e callback con Svelte 5 e TypeScript"
  />
</svelte:head>

<main>
  <h1>Componenti, props ed eventi</h1>

  <p>
    Due istanze dello stesso componente,
    con valori e comportamenti indipendenti.
  </p>

  <Counter
    title="Primo contatore"
    value={firstCount}
    onIncrement={() => firstCount++}
    onReset={() => firstCount = 0}
  />

  <Counter
    title="Secondo contatore"
    value={secondCount}
    onIncrement={() => secondCount++}
    onReset={() => secondCount = 10}
  />
</main>

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

  h1 {
    margin-bottom: 0.75rem;
  }

  main > p {
    margin-bottom: 2rem;
  }

  main > :global(section) {
    margin-bottom: 1rem;
  }
</style>

Abbiamo importato Counter e lo abbiamo inserito due volte nel markup. Entrambe le istanze utilizzano lo stesso file Counter.svelte, ma ricevono valori iniziali, titoli e callback differenti.

La prima istanza parte da zero e viene azzerata a zero. La seconda parte da dieci e, quando premiamo «Azzera», ritorna a dieci. Non abbiamo dovuto duplicare il codice del componente: abbiamo cambiato soltanto le props.

Le variabili firstCount e secondCount sono dichiarate con $state(). Quando una callback le modifica, Svelte aggiorna il valore passato alla rispettiva istanza. I due contatori sono indipendenti perché ciascuno riceve una variabile diversa.

In questo modo abbiamo costruito un componente riutilizzabile e abbiamo separato la responsabilità della visualizzazione da quella della gestione dei dati.

Verificare il risultato

Se il server di sviluppo è già attivo, Vite aggiornerà la pagina quando salviamo i file. In caso contrario, dalla directory del progetto eseguiamo:

npm run dev

Apriamo l'indirizzo locale mostrato nel terminale. Dovremmo vedere due riquadri con titoli differenti e valori iniziali rispettivamente pari a zero e dieci.

Premendo «Incrementa» nel primo riquadro, cambierà soltanto il primo valore. Premendo «Incrementa» nel secondo, cambierà soltanto il secondo. Anche il comportamento di «Azzera» sarà indipendente per ciascuna istanza.

Se il progetto include lo script check, possiamo verificare anche i tipi e gli errori del codice:

npm run check

Per controllare che l'applicazione venga compilata correttamente per la produzione, eseguiamo:

npm run build

Gli script disponibili dipendono dalla configurazione del progetto. Se uno dei due comandi non è definito, possiamo controllare la sezione scripts del file package.json.

Errori comuni da evitare

Un errore frequente consiste nel tentare di modificare direttamente una prop all'interno del componente figlio. Nel nostro esempio non dobbiamo scrivere value++ dentro Counter.svelte, perché value appartiene al componente padre ed è ricevuto dall'esterno.

Un altro errore è confondere il passaggio di una funzione con la sua esecuzione. Scrivendo onIncrement={...} nel componente padre, passiamo una funzione al figlio. Sarà il figlio a invocarla quando si verifica l'evento. Se eseguissimo la funzione direttamente durante il rendering, il comportamento sarebbe diverso da quello desiderato.

Infine, non bisogna confondere i componenti con le pagine. Un componente è un'unità riutilizzabile dell'interfaccia; una pagina può essere composta da molti componenti. Il routing e l'organizzazione delle pagine saranno affrontati più avanti, quando introdurremo SvelteKit.

Conclusione

In questo tutorial abbiamo imparato a creare un componente Svelte separato, definire le props con TypeScript, ricevere i dati tramite $props() e comunicare dal figlio al padre attraverso funzioni callback.

Abbiamo inoltre verificato come lo stesso componente possa essere riutilizzato più volte con valori e comportamenti differenti. Questo approccio è fondamentale per costruire applicazioni frontend ordinate, testabili e facili da mantenere.

Nel prossimo tutorial passeremo ai form e al binding dei dati. Vedremo come acquisire i valori inseriti dall'utente, collegarli allo stato dell'applicazione e implementare una prima validazione. Sarà il passo necessario per costruire interfacce capaci di raccogliere dati da inviare successivamente al backend Express.