Salta al contenuto principale

02 - $state e $derived: Reattivita Universale con i Runes

$state e $derived: Reattivita Universale con i Runes di Svelte 5 Il sistema di reattivita e il cuore di qualsiasi framework UI moderno.

Cosa ne pensi di questo articolo?

02 - $state e $derived: Reattivita Universale con i Runes
Svelte 5Foto:Brett SaylesPexels

In Breve

$state e $derived: Reattivita Universale con i Runes di Svelte 5 Il sistema di reattivita e il cuore di qualsiasi framework UI moderno.

$state e $derived: Reattivita Universale con i Runes di Svelte 5

Il sistema di reattivita e il cuore di qualsiasi framework UI moderno. Svelte 5 ha completamente ridisegnato come la reattivita funziona, passando dal "magic assignment" di Svelte 4 ai Runes: funzioni speciali con prefisso $ che comunicano direttamente con il compilatore. I due Runes fondamentali sono $state e $derived, che coprono il 90% dei casi d'uso della reattivita in qualsiasi applicazione Svelte.

Questa guida approfondisce come funzionano internamente questi due Runes, perche usano Proxy ES6 per la reattivita profonda, come $derived implementa la memoizzazione automatica, e - cosa piu importante - come possono essere usati al di fuori dei file .svelte in normali moduli TypeScript, abilitando pattern di state management completamente nuovi.

Prerequisiti

  • Conoscenza base di Svelte 5 (vedi articolo 1: Approccio Compiler-Driven)
  • TypeScript intermedio (generics, Proxy, getter/setter)
  • Concetto di memoizzazione e dependency tracking

$state: Stato Reattivo con Proxy ES6

In Svelte 4, qualsiasi variabile dichiarata nel blocco <script> era automaticamente reattiva: il compilatore tracciava ogni assegnamento e generava codice di update. Questo funzionava per tipi primitivi, ma per oggetti e array aveva limitazioni: modifiche "profonde" come array.push() o obj.nested.prop = value non venivano tracciate.

Svelte 5 risolve questo con $state, che usa Proxy ES6 per intercettare ogni accesso e modifica all'oggetto, inclusi quelli annidati. Il risultato e una reattivita profonda vera:

<script lang="ts">
  // Tipi primitivi: reattivita diretta
  let count = $state(0);
  let name = $state('Federico');
  let isVisible = $state(true);

  // Oggetti: reattivita profonda tramite Proxy
  let user = $state({
    name: 'Federico',
    address: {
      city: 'Bari',
      country: 'IT'
    },
    tags: ['developer', 'writer']
  });

  function updateCity() {
    // Questo funziona! La modifica profonda e tracciata
    user.address.city = 'Milano';
  }

  function addTag(tag: string) {
    // Anche push() e tracciato grazie al Proxy
    user.tags.push(tag);
  }
</script>

<p>Citta: {user.address.city}</p>
<p>Tags: {user.tags.join(', ')}</p>

Con Svelte 4, user.address.city = 'Milano' non avrebbe triggerato un update della UI perche il compilatore tracciava solo assegnamenti alla variabile top-level (user = ...). Con Svelte 5 e il Proxy, ogni accesso a qualsiasi livello di profondita e intercettato.

Come Funziona il Proxy di $state Internamente

Per capire la reattivita profonda, bisogna capire come ES6 Proxy funziona e come Svelte lo usa:

// Implementazione concettuale del Proxy usato da $state
// (semplificata rispetto al codice reale di Svelte 5)
function createReactiveProxy<T extends object>(
  target: T,
  onChange: () => void
): T {
  return new Proxy(target, {
    get(obj, prop) {
      const value = Reflect.get(obj, prop);

      // Se il valore e un oggetto/array, crea un Proxy annidato
      if (value !== null && typeof value === 'object') {
        return createReactiveProxy(value, onChange);
      }

      return value;
    },

    set(obj, prop, value) {
      const result = Reflect.set(obj, prop, value);
      // Notifica gli osservatori che qualcosa e cambiato
      onChange();
      return result;
    }
  });
}

In pratica, quando scrivi user.address.city = 'Milano', succede questo:

  1. Il Proxy esterno intercetta .address e ritorna un Proxy annidato per address
  2. Il Proxy annidato intercetta .city = 'Milano'
  3. Aggiorna il valore e notifica il sistema di reattivita di Svelte
  4. Svelte schedula un aggiornamento solo per i binding che dipendono da user.address.city

Quando il Proxy NON Funziona

Ci sono casi in cui la reattivita tramite Proxy non funziona come atteso:

  • Destructuring perde la reattivita: const { city } = user.address; - city e ora una stringa primitiva, non un Proxy. Usa sempre user.address.city direttamente nel template.
  • Spread perde la reattivita: const copy = { ...user }; crea una copia non-reattiva.
  • Map e Set richiedono $state.raw per array custom: Non tutti i metodi di collezioni esotiche sono patchati. Per Map/Set, usa $state(new Map()) - Svelte 5 patcha anche questi.

$state con TypeScript: Typing Completo

$state e completamente integrato con TypeScript. Il tipo viene inferito automaticamente dal valore iniziale, ma puoi essere esplicito con le generics:

<script lang="ts">
  // Inferenza automatica del tipo
  let count = $state(0);           // number
  let name = $state('');           // string
  let flag = $state(false);        // boolean

  // Tipo esplicito per casi complessi
  interface User {
    id: number;
    name: string;
    role: 'admin' | 'user';
    metadata: Record<string, unknown>;
  }

  let user = $state<User>({
    id: 1,
    name: 'Federico',
    role: 'admin',
    metadata: {}
  });

  // Array con tipo esplicito
  let items = $state<string[]>([]);

  // Stato nullable (inizializzato a null)
  let selectedItem = $state<User | null>(null);

  // $state.raw: reattivita superficiale (solo l'assegnamento, non le mutazioni)
  // Utile per grandi array dove la performance conta
  let bigList = $state.raw<number[]>([]);

  function addToList(n: number) {
    // Con $state.raw, devi riassegnare (non push)
    bigList = [...bigList, n];
  }
</script>

$derived: Memoizzazione Automatica

$derived crea valori computati che vengono ricalcolati solo quando le loro dipendenze cambiano. E l'equivalente di useMemo di React, ma senza dover dichiarare esplicitamente le dipendenze - il compilatore le traccia automaticamente analizzando il codice della funzione.

<script lang="ts">
  let items = $state([
    { id: 1, name: 'Laptop', price: 1200, category: 'tech' },
    { id: 2, name: 'Mouse', price: 25, category: 'tech' },
    { id: 3, name: 'Desk', price: 450, category: 'furniture' }
  ]);

  let filterCategory = $state('');
  let sortBy = $state<'name' | 'price'>('name');
  let sortAsc = $state(true);

  // $derived traccia automaticamente: items, filterCategory, sortBy, sortAsc
  const filteredAndSorted = $derived(() => {
    let result = filterCategory
      ? items.filter(i => i.category === filterCategory)
      : [...items];

    result.sort((a, b) => {
      const mult = sortAsc ? 1 : -1;
      if (sortBy === 'name') return a.name.localeCompare(b.name) * mult;
      return (a.price - b.price) * mult;
    });

    return result;
  });

  // Dipende solo da filteredAndSorted
  const totalValue = $derived(
    filteredAndSorted.reduce((sum, item) => sum + item.price, 0)
  );

  // Dipende da items direttamente
  const categories = $derived([...new Set(items.map(i => i.category))]);
</script>

Svelte 5 usa un sistema push-pull per la memoizzazione: quando una dipendenza cambia, $derived viene marcato come "dirty" (push), ma il valore non viene ricalcolato subito. Viene ricalcolato solo quando qualcuno lo legge (pull). Se nessuno legge il derived, il calcolo non avviene.

$derived.by: Computazioni Complesse con Funzione

Per computazioni che richiedono piu di un'espressione, usa $derived.by() che accetta una funzione con corpo:

<script lang="ts">
  let transactions = $state([
    { amount: 100, type: 'income', date: new Date('2026-01-15') },
    { amount: -50, type: 'expense', date: new Date('2026-01-16') },
    { amount: 200, type: 'income', date: new Date('2026-02-01') },
    { amount: -75, type: 'expense', date: new Date('2026-02-15') }
  ]);

  // Per computazioni multi-step, usa $derived.by
  const stats = $derived.by(() => {
    const income = transactions
      .filter(t => t.type === 'income')
      .reduce((sum, t) => sum + t.amount, 0);

    const expenses = transactions
      .filter(t => t.type === 'expense')
      .reduce((sum, t) => sum + Math.abs(t.amount), 0);

    const balance = income - expenses;

    const byMonth = transactions.reduce((acc, t) => {
      const key = t.date.toISOString().slice(0, 7); // YYYY-MM
      acc[key] = (acc[key] ?? 0) + t.amount;
      return acc;
    }, {} as Record<string, number>);

    return { income, expenses, balance, byMonth };
  });
</script>

<p>Saldo: {stats.balance}</p>
<p>Entrate: {stats.income} - Uscite: {stats.expenses}</p>

Runes in File TypeScript: La Novita Rivoluzionaria

La differenza piu importante tra Svelte 4 e Svelte 5 non e la sintassi dei Runes, ma il fatto che i Runes funzionano in qualsiasi file con estensione .svelte.ts o .svelte.js, non solo nei componenti. Questo abilita pattern di state management completamente nuovi.

// src/lib/stores/cart.svelte.ts
// Un "store" costruito con i Runes, senza dipendenze da Svelte stores

export interface CartItem {
  id: number;
  name: string;
  price: number;
  quantity: number;
}

function createCart() {
  let items = $state<CartItem[]>([]);

  // Computati derivati dallo stato del carrello
  const total = $derived(
    items.reduce((sum, item) => sum + item.price * item.quantity, 0)
  );

  const itemCount = $derived(
    items.reduce((sum, item) => sum + item.quantity, 0)
  );

  const isEmpty = $derived(items.length === 0);

  function addItem(item: Omit<CartItem, 'quantity'>) {
    const existing = items.find(i => i.id === item.id);
    if (existing) {
      existing.quantity++;  // Mutazione diretta: il Proxy la traccia
    } else {
      items.push({ ...item, quantity: 1 });
    }
  }

  function removeItem(id: number) {
    const index = items.findIndex(i => i.id === id);
    if (index !== -1) items.splice(index, 1);
  }

  function updateQuantity(id: number, quantity: number) {
    const item = items.find(i => i.id === id);
    if (item) {
      if (quantity <= 0) removeItem(id);
      else item.quantity = quantity;
    }
  }

  function clear() {
    items = [];
  }

  return {
    // Esponi come getter per proteggere l'accesso diretto
    get items() { return items; },
    get total() { return total; },
    get itemCount() { return itemCount; },
    get isEmpty() { return isEmpty; },
    addItem,
    removeItem,
    updateQuantity,
    clear
  };
}

// Singleton: un'istanza condivisa tra tutti i componenti che importano questo modulo
export const cart = createCart();

Questo store puo essere importato in qualsiasi componente .svelte e usato direttamente, con piena reattivita:

<!-- CartSummary.svelte -->
<script lang="ts">
  import { cart } from '$lib/stores/cart.svelte';
</script>

{#if cart.isEmpty}
  <p>Il carrello e vuoto</p>
{:else}
  <p>{cart.itemCount} prodotti - Totale: €{cart.total.toFixed(2)}</p>
  {#each cart.items as item (item.id)}
    <div>
      <span>{item.name}</span>
      <button onclick={() => cart.updateQuantity(item.id, item.quantity - 1)}>-</button>
      <span>{item.quantity}</span>
      <button onclick={() => cart.updateQuantity(item.id, item.quantity + 1)}>+</button>
      <button onclick={() => cart.removeItem(item.id)}>Rimuovi</button>
    </div>
  {/each}
  <button onclick={cart.clear}>Svuota carrello</button>
{/if}

$state nelle Classi TypeScript

I Runes funzionano anche nelle classi TypeScript, abilitando un pattern OOP-style per lo state management:

// src/lib/models/todo-list.svelte.ts
export class TodoList {
  // I campi della classe usano $state come inizializzatore
  items = $state<{ id: number; text: string; done: boolean }[]>([]);
  filter = $state<'all' | 'active' | 'done'>('all');

  // $derived come getter calcolato
  readonly filtered = $derived.by(() => {
    switch (this.filter) {
      case 'active': return this.items.filter(i => !i.done);
      case 'done': return this.items.filter(i => i.done);
      default: return this.items;
    }
  });

  readonly stats = $derived.by(() => ({
    total: this.items.length,
    done: this.items.filter(i => i.done).length,
    active: this.items.filter(i => !i.done).length
  }));

  private nextId = 1;

  add(text: string) {
    this.items.push({ id: this.nextId++, text, done: false });
  }

  toggle(id: number) {
    const item = this.items.find(i => i.id === id);
    if (item) item.done = !item.done;
  }

  delete(id: number) {
    this.items = this.items.filter(i => i.id !== id);
  }

  clearDone() {
    this.items = this.items.filter(i => !i.done);
  }
}

// Puo essere usata come singleton o istanziata per componente
export const todoList = new TodoList();

Confronto con React useMemo e Vue computed

Il tracking automatico delle dipendenze di $derived elimina la principale fonte di bug nelle soluzioni equivalenti di React e Vue:

// React: dipendenze manuali, fonte di bug
const filteredItems = useMemo(() => {
  return items.filter(i => i.category === filter);
}, [items, filter]); // Se dimentichi una dipendenza: bug silenzioso

// Vue 3: tracking automatico, ma sintassi verbosa
const filteredItems = computed(() => {
  return items.value.filter(i => i.category === filter.value);
});

// Svelte 5: tracking automatico, sintassi diretta
const filteredItems = $derived(
  items.filter(i => i.category === filter)
);

La differenza pratica: con React, dimenticare una dipendenza nell'array di useMemo produce valori obsoleti silenziosi, un bug notoriamente difficile da debuggare. Svelte 5 non ha questo problema perche il tracking avviene a runtime tramite Proxy.

Quando Usare $state vs $derived

  • Usa $state per valori che cambiano in risposta ad azioni utente o eventi (input fields, toggle switches, dati caricati da API)
  • Usa $derived per qualsiasi cosa che si puo calcolare da $state esistente (totali, filtri, formattazioni, trasformazioni)
  • Non usare $effect per calcoli - se ti trovi a scrivere $effect(() => { derived = compute(state); }), usa $derived invece

Pattern Avanzato: Reactivity Context Condiviso

Un pattern molto utile in applicazioni complesse e creare un "context" reattivo che viene condiviso tra componenti in una gerarchia:

// src/lib/context/theme.svelte.ts
import { getContext, setContext } from 'svelte';

const THEME_KEY = Symbol('theme');

export function createThemeContext() {
  let isDark = $state(
    typeof window !== 'undefined'
      ? localStorage.getItem('theme') === 'dark'
      : false
  );

  const theme = $derived(isDark ? 'dark' : 'light');
  const cssClass = $derived(`theme-${theme}`);

  $effect(() => {
    if (typeof window !== 'undefined') {
      localStorage.setItem('theme', theme);
      document.documentElement.className = cssClass;
    }
  });

  function toggle() { isDark = !isDark; }
  function setDark(value: boolean) { isDark = value; }

  const context = { get isDark() { return isDark; }, get theme() { return theme; }, toggle, setDark };
  setContext(THEME_KEY, context);
  return context;
}

export function useTheme() {
  return getContext<ReturnType<typeof createThemeContext>>(THEME_KEY);
}

Conclusioni e Prossimi Passi

$state e $derived sono i due pilastri della reattivita in Svelte 5. La loro capacita di funzionare al di fuori dei componenti .svelte apre possibilita architetturali che Svelte 4 non consentiva: state management modulare, logica testabile in isolamento, pattern OOP con reattivita integrata, e condivisione di stato tra componenti senza Context API esplicite.

Il prossimo articolo esamina $effect - il Rune per i side effects - e spiega quando usarlo correttamente e quando invece dovresti usare $derived. E un topic cruciale perche l'abuso di $effect e il principale anti-pattern in Svelte 5.

Serie: Svelte 5 e Frontend Compiler-Driven

  • Articolo 1: Approccio Compiler-Driven e Modello Mentale
  • Articolo 2 (questo): $state e $derived - Reattivita Universale con i Runes
  • Articolo 3: $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