$state și $derived: Reactivitate universală cu Svelte 5 Runes
Sistemul de receptivitate este inima oricărui cadru UI modern. Svelte 5 a fost complet reproiectat
cum funcționează reactivitatea, trecând de la „sarcina magică” a lui Svelte 4 la Rune: funcții
special cu prefix $ care comunică direct cu compilatorul. Cele două Rune fundamentale
eu sunt $state e $derived, care acoperă 90% din cazurile de utilizare de receptivitate în
orice aplicație Svelte.
Acest ghid analizează modul în care aceste două rune funcționează intern, de ce folosesc ES6 Proxy pentru
reactivitate profundă, cum ar fi $derived implementează memorarea automată și — ce mai
important — cum pot fi utilizate în afara fișierelor .svelte în modulele TypeScript obișnuite, permițând
modele complet noi de management de stat.
Cerințe preliminare
- Cunoștințe de bază despre Svelte 5 (vezi articolul 1: Abordare bazată pe compilator)
- TypeScript intermediar (generice, proxy, getter/setter)
- Conceptul de memorare și urmărire a dependenței
$state: stare reactivă cu proxy ES6
În Svelte 4, orice variabilă declarată în bloc <script> a fost automat
reactiv: compilatorul a urmărit fiecare atribuire și a generat codul de actualizare. Asta a funcționat pentru
tipuri primitive, dar pentru obiecte și matrice avea limitări: modificări „profunde” precum
array.push() o obj.nested.prop = value nu au fost urmărite.
Svelte 5 rezolvă asta cu $state, pe care îl folosește Proxy ES6 a intercepta fiecare
accesul și modificarea obiectelor, inclusiv cele imbricate. Rezultatul este o adevărată reactivitate profundă:
<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>
Cu Svelte 4, user.address.city = 'Milano' nu ar fi declanșat o actualizare a UI
deoarece compilatorul a desenat doar atribuiri pentru variabila de nivel superior (user = ...).
Cu Svelte 5 și Proxy, orice acces la orice nivel de adâncime este interceptat.
Cum funcționează intern proxy-ul $state
Pentru a înțelege capacitatea de răspuns profundă, trebuie să înțelegeți cum funcționează ES6 Proxy și cum îl folosește Svelte:
// 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;
}
});
}
Practic, când scrii user.address.city = 'Milano', asta se intampla:
- Proxy-ul extern interceptează
.addressși returnează un proxy imbricat pentruaddress - Interceptări imbricate de proxy
.city = 'Milano' - Actualizați valoarea și notificați sistemul de răspuns al lui Svelte
- Svelte programează o actualizare numai pentru legăturile care depind de
user.address.city
Când proxy-ul NU FUNCȚIONEAZĂ
Există cazuri în care receptivitatea prin proxy nu funcționează conform așteptărilor:
-
Destructurarea își pierde capacitatea de răspuns:
const { city } = user.address;—cityși acum un șir primitiv, nu un proxy. Utilizați întotdeaunauser.address.citydirect în șablon. -
Răspândirea își pierde reactivitatea:
const copy = { ...user };creează o copie nereactivă. -
Map and Set necesită $state.raw pentru matrice personalizate:
Nu toate metodele de colectare exotică sunt patch-uri. Pentru Hartă/Set, utilizați
$state(new Map())— Quick 5 patch-uri și pe acestea.
$state cu TypeScript: Tastare completă
$state și complet integrat cu TypeScript. Tipul este dedus automat
de la valoarea inițială, dar puteți fi explicit cu generice:
<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>
$derivat: Stocare automată
$derived creează valori calculate care sunt recalculate numai atunci când dependențele lor se modifică.
Și echivalentul lui useMemo de React, dar fără a fi nevoie să declare în mod explicit dependențe -
compilatorul le urmărește automat analizând codul funcției.
<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 folosește a sistem push-pull pentru memorare: când o dependență se schimbă,
$derived este marcat ca „murdar” (push), dar valoarea nu este recalculată imediat.
Se recalculează doar când cineva îl citește (trage). Dacă nimeni nu citește derivatul, calculul nu are loc.
$derived.by: calcule complexe cu funcție
Pentru calcule care necesită mai mult de o expresie, utilizați $derived.by() pe care o acceptă
o funcție cu corp:
<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>
Rune în fișiere TypeScript: știrile revoluționare
Cea mai importantă diferență dintre Svelte 4 și Svelte 5 nu este sintaxa Runelor, ci faptul că Runele funcționează în orice fișier cu extensia .svelte.ts sau .svelte.js, nu numai atât în componente. Acest lucru permite modele complet noi de management de stat.
// 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();
Acest magazin poate fi importat în orice componentă .svelte și folosit direct, cu full reactivitate:
<!-- 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 în clasele TypeScript
Runele funcționează și în clasele TypeScript, permițând un model în stil OOP pentru gestionarea stării:
// 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();
Comparația cu React useMemo și Vue calculate
Urmărirea automată a dependenței $derived elimină principala sursă de bug-uri
în soluțiile echivalente React și 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)
);
Diferența practică: cu React, uitați de o dependență în matricea de useMemo produce
valori învechite silențioase, un bug notoriu de dificil de depanat. Svelte 5 nu are această problemă
deoarece urmărirea are loc în timpul execuției prin Proxy.
Când să folosiți $state vs $derived
- Folosiți $state pentru valorile care se modifică ca răspuns la acțiunile sau evenimentele utilizatorului (câmpuri de intrare, comutatoare de comutare, date încărcate din API)
- Utilizați $derivat pentru orice poate fi calculat din $state existent (totaluri, filtre, formatare, transformări)
-
Nu utilizați $efect pentru calcule — dacă te trezești scriind
$effect(() => { derived = compute(state); }), STATELE UNITE ALE AMERICII$derivedÎn schimb
Model avansat: context de reactivitate partajată
Un model foarte util în aplicații complexe și creând un „context” reactiv care este partajat între componente dintr-o ierarhie:
// 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);
}
Concluzii și pașii următori
$state e $derived ei sunt cei doi piloni ai reactivitatii din Svelte 5. Ai lor
capacitatea de a funcționa în afara componentelor .svelte deschide posibilități arhitecturale pe care Svelte 4
nu a permis: gestionarea modulară a stării, logica testabilă izolat, modele OOP cu reactivitate
integrate și partajarea stării între componente fără API-uri context explicite.
Următorul articol examinează $efect — Runa pentru efecte secundare — și explică când
folosește-l corect și când ar trebui să îl folosești în schimb $derived. Este un subiect crucial pentru că
abuzul de $effect și principalul anti-model din Svelte 5.
Seria: Svelte 5 și Frontend Compiler-Driven
- Articolul 1: Abordare condusă de compilator și model mental
- Articolul 2 (acesta): $state și $derived — Reactivitate universală cu rune
- Articolul 3: $effect și ciclul de viață – Când să-l utilizați (și când nu)
- Articolul 4: SvelteKit SSR, Streaming și Funcții de încărcare
- Articolul 5: Tranziții și animații în Svelte 5
- Articolul 6: Accesibilitate în Svelte: Avertismente ale compilatorului și cele mai bune practici
- Articolul 7: Managementul global al statului: context, rune și magazine
- Articolul 8: Migrarea de la Svelte 4 la Svelte 5 — Ghid practic
- Articolul 9: Testarea în Svelte 5: Vitest, Biblioteca de testare și dramaturg







