$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:
- Il Proxy esterno intercetta
.addresse ritorna un Proxy annidato peraddress - Il Proxy annidato intercetta
.city = 'Milano' - Aggiorna il valore e notifica il sistema di reattivita di Svelte
- 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;—citye ora una stringa primitiva, non un Proxy. Usa sempreuser.address.citydirettamente 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$derivedinvece
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







