$state en $derived: Universele reactiviteit met Svelte 5 Runes
Het responsiviteitssysteem vormt het hart van elk modern UI-framework. Svelte 5 is volledig opnieuw ontworpen
hoe reactiviteit werkt, van de "magische opdracht" van Svelte 4 naar Runen: functies
speciaal met voorvoegsel $ die rechtstreeks communiceren met de compiler. De twee fundamentele Runen
Ik ben $state e $derived, die 90% van de gebruiksscenario's voor responsiviteit bestrijken
elke Svelte-toepassing.
Deze gids gaat dieper in op hoe deze twee Runen intern werken, waarom ze ES6 Proxy gebruiken voor de
diepe reactiviteit, zoals $derived implementeert automatische memoisatie, en - wat nog meer
belangrijk - hoe ze kunnen worden gebruikt buiten .svelte-bestanden in reguliere TypeScript-modules, waardoor
compleet nieuwe staatsmanagementpatronen.
Vereisten
- Basiskennis van Svelte 5 (zie artikel 1: Compiler-Driven Approach)
- Gemiddeld typescript (generiek, proxy, getter/setter)
- Concept voor het onthouden en volgen van afhankelijkheid
$state: reactieve status met ES6 Proxy
In Svelte 4: elke variabele die in het blok wordt gedeclareerd <script> het ging automatisch
reactief: de compiler volgde elke toewijzing en genereerde updatecode. Dit werkte voor
primitieve typen, maar voor objecten en arrays had het beperkingen: "diepe" veranderingen zoals
array.push() o obj.nested.prop = value ze werden niet gevolgd.
Svelte 5 lost dit op met $state, die hij gebruikt ES6-proxy ieder onderscheppen
toegang tot en wijziging van objecten, inclusief geneste objecten. Het resultaat is echte diepe reactiviteit:
<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>
Met Svelte 4, user.address.city = 'Milano' het zou geen UI-update hebben geactiveerd
omdat de compiler alleen toewijzingen trok aan de variabele op het hoogste niveau (user = ...).
Met Svelte 5 en de Proxy wordt elke toegang op elk diepteniveau onderschept.
Hoe de $state-proxy intern werkt
Om diepgaande responsiviteit te begrijpen, moet u begrijpen hoe ES6 Proxy werkt en hoe Svelte het gebruikt:
// 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;
}
});
}
Kortom, als je schrijft user.address.city = 'Milano', dit gebeurt:
- De externe proxy onderschept
.addressen retourneert een geneste proxy vooraddress - Geneste proxy onderschept
.city = 'Milano' - Update de waarde en breng het reactiesysteem van Svelte op de hoogte
- Svelte plant alleen een update voor bindingen die afhankelijk zijn van
user.address.city
Wanneer de proxy NIET werkt
Er zijn gevallen waarin het reactievermogen via Proxy niet werkt zoals verwacht:
-
Bij destructurering gaat het reactievermogen verloren:
const { city } = user.address;—cityen nu een primitieve string, geen proxy. Altijd gebruikenuser.address.citydirect in de sjabloon. -
Verspreiding verliest reactiviteit:
const copy = { ...user };creëert een niet-reactieve kopie. -
Map en Set vereisen $state.raw voor aangepaste arrays:
Niet alle exotische verzamelmethoden zijn gepatcht. Gebruik voor Kaart/Set
$state(new Map())- Quick 5 patch deze ook.
$state met TypeScript: volledig typen
$state en volledig geïntegreerd met TypeScript. Het type wordt automatisch afgeleid
vanaf de beginwaarde, maar u kunt expliciet zijn met generieke geneesmiddelen:
<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>
$afgeleid: automatische opslag
$derived creëert berekende waarden die alleen opnieuw worden berekend als hun afhankelijkheden veranderen.
En het equivalent van useMemo van React, maar zonder expliciet afhankelijkheden te hoeven aangeven –
de compiler volgt ze automatisch door de functiecode te analyseren.
<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 maakt gebruik van een push-pull-systeem ter herinnering: wanneer een afhankelijkheid verandert,
$derived wordt gemarkeerd als "vuil" (push), maar de waarde wordt niet onmiddellijk opnieuw berekend.
Het wordt pas opnieuw berekend als iemand het leest (pull). Als niemand de afgeleide leest, vindt de berekening niet plaats.
$derived.by: complexe berekeningen met functie
Voor berekeningen die meer dan één uitdrukking vereisen, gebruikt u $derived.by() die hij accepteert
een functie met body:
<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>
Runen in TypeScript-bestanden: het revolutionaire nieuws
Het belangrijkste verschil tussen Svelte 4 en Svelte 5 is niet de syntaxis van de Runen, maar het feit dat Runen werken in elk bestand met de extensie .svelte.ts of .svelte.js, niet alleen dat in de componenten. Dit maakt volledig nieuwe staatsmanagementpatronen mogelijk.
// 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();
Deze winkel kan in elk .svelte-onderdeel worden geïmporteerd en direct worden gebruikt, met full reactiviteit:
<!-- 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 in TypeScript-klassen
Runen werken ook in TypeScript-klassen, waardoor een patroon in OOP-stijl voor statusbeheer mogelijk wordt:
// 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();
Vergelijking met React useMemo en Vue berekend
Automatische tracking van afhankelijkheid $derived elimineert de belangrijkste bron van bugs
in de gelijkwaardige React- en Vue-oplossingen:
// 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)
);
Het praktische verschil: met React vergeet u een afhankelijkheid in de reeks van useMemo produceert
stille verouderde waarden, een notoir moeilijke bug om te debuggen. Svelte 5 heeft dit probleem niet
omdat tracking plaatsvindt tijdens runtime via proxy.
Wanneer moet u $state versus $derived gebruiken?
- Gebruik $state voor waarden die veranderen als reactie op gebruikersacties of gebeurtenissen (invoervelden, tuimelschakelaars, gegevens geladen vanuit API)
- Gebruik $afgeleid voor alles dat kan worden berekend op basis van de bestaande $state (totalen, filters, opmaak, transformaties)
-
Gebruik $effect niet voor berekeningen – als je merkt dat je schrijft
$effect(() => { derived = compute(state); }), VS$derivedIn plaats van
Geavanceerd patroon: context van gedeelde reactiviteit
Een zeer nuttig patroon in complexe toepassingen en het creëren van een reactieve "context" die wordt gedeeld tussen componenten in een hiërarchie:
// 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);
}
Conclusies en volgende stappen
$state e $derived zij zijn de twee pijlers van reactiviteit in Svelte 5. Die van hen
Het vermogen om buiten .svelte-componenten te functioneren opent architectonische mogelijkheden die Svelte 4
stond niet toe: modulair statusbeheer, logica die afzonderlijk kan worden getest, OOP-patronen met reactiviteit
geïntegreerd en deelt de status tussen componenten zonder expliciete Context-API's.
Het volgende artikel onderzoekt $effect – de Rune voor bijwerkingen – en legt uit wanneer
gebruik het op de juiste manier en wanneer u het in plaats daarvan zou moeten gebruiken $derived. Het is een cruciaal onderwerp omdat
het misbruik van $effect en het belangrijkste antipatroon in Svelte 5.
Serie: Svelte 5 en frontend-compilergestuurd
- Artikel 1: Compilergestuurde aanpak en mentaal model
- Artikel 2 (dit): $state en $derived - Universele reactiviteit met runen
- Artikel 3: $effect en de levenscyclus – wanneer moet je het gebruiken (en wanneer niet)
- Artikel 4: SvelteKit SSR, streaming en laadfuncties
- Artikel 5: Overgangen en animaties in Svelte 5
- Artikel 6: Toegankelijkheid in Svelte: compilerwaarschuwingen en best practices
- Artikel 7: Mondiaal staatsbeheer: context, runen en winkels
- Artikel 8: Migreren van Svelte 4 naar Svelte 5 — Praktische gids
- Artikel 9: Testen in Svelte 5: Vitest, Testbibliotheek en Toneelschrijver







