$state and $derived: Universal Reactivity with Svelte 5 Runes
The responsiveness system is the heart of any modern UI framework. Svelte 5 has completely redesigned
how reactivity works, moving from the "magic assignment" of Svelte 4 to Runes: functions
special with prefix $ which communicate directly with the compiler. The two fundamental Runes
I am $state e $derived, which cover 90% of responsiveness use cases in
any Svelte application.
This guide delves into how these two Runes work internally, why they use ES6 Proxy for the
deep reactivity, like $derived implements automatic memoization, and — what more
important — how they can be used outside of .svelte files in regular TypeScript modules, enabling
completely new state management patterns.
Prerequisites
- Basic knowledge of Svelte 5 (see article 1: Compiler-Driven Approach)
- Intermediate TypeScript (generics, Proxy, getter/setter)
- Memoization and dependency tracking concept
$state: Reactive state with ES6 Proxy
In Svelte 4, any variable declared in the block <script> it was automatically
reactive: the compiler tracked each assignment and generated update code. This worked for
primitive types, but for objects and arrays it had limitations: "deep" changes like
array.push() o obj.nested.prop = value they were not tracked.
Svelte 5 solves this with $state, which he uses ES6 Proxy to intercept every
object access and modification, including nested ones. The result is true deep reactivity:
<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>
With Svelte 4, user.address.city = 'Milano' it would not have triggered a UI update
because the compiler only drew assignments to the top-level variable (user = ...).
With Svelte 5 and the Proxy, any access at any depth level is intercepted.
How the $state Proxy Works Internally
To understand deep responsiveness, you need to understand how ES6 Proxy works and how Svelte uses it:
// 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;
}
});
}
Basically, when you write user.address.city = 'Milano', this happens:
- The external proxy intercepts
.addressand returns a nested Proxy foraddress - Nested Proxy intercepts
.city = 'Milano' - Update the value and notify Svelte's responsiveness system
- Svelte schedules an update only for bindings that depend on
user.address.city
When the Proxy DOESN'T Work
There are cases where responsiveness via Proxy does not work as expected:
-
Destructuring loses responsiveness:
const { city } = user.address;—cityand now a primitive string, not a Proxy. Always useuser.address.citydirectly in the template. -
Spread loses reactivity:
const copy = { ...user };creates a non-reactive copy. -
Map and Set require $state.raw for custom arrays:
Not all exotic collections methods are patched. For Map/Set, use
$state(new Map())— Quick 5 patch these too.
$state with TypeScript: Full Typing
$state and fully integrated with TypeScript. The type is automatically inferred
from the initial value, but you can be explicit with 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: Automatic Storage
$derived creates computed values that are recomputed only when their dependencies change.
And the equivalent of useMemo of React, but without having to explicitly declare dependencies —
the compiler automatically tracks them by analyzing the function code.
<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 uses a push-pull system for memoization: when a dependency changes,
$derived is marked as "dirty" (push), but the value is not recalculated immediately.
It is only recalculated when someone reads it (pull). If no one reads the derived, the calculation does not occur.
$derived.by: Complex Computations with Function
For computations requiring more than one expression, use $derived.by() which he accepts
a function with 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>
Runes in TypeScript Files: The Revolutionary News
The most important difference between Svelte 4 and Svelte 5 is not the syntax of the Runes, but the fact that Runes work in any file with the .svelte.ts or .svelte.js extension, not only that in the components. This enables completely new state management patterns.
// 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();
This store can be imported into any .svelte component and used directly, with full reactivity:
<!-- 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 Classes
Runes also work in TypeScript classes, enabling an OOP-style pattern for 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();
Comparison with React useMemo and Vue computed
Automatic dependency tracking $derived eliminates the main source of bugs
in the equivalent React and Vue solutions:
// 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)
);
The practical difference: with React, forget about a dependency in the array of useMemo produces
silent obsolete values, a notoriously difficult bug to debug. Svelte 5 does not have this problem
because tracking occurs at runtime via Proxy.
When to Use $state vs $derived
- Use $state for values that change in response to user actions or events (input fields, toggle switches, data loaded from API)
- Use $derived for anything that can be computed from existing $state (totals, filters, formatting, transformations)
-
Don't use $effect for calculations — if you find yourself writing
$effect(() => { derived = compute(state); }), USA$derivedInstead
Advanced Pattern: Shared Reactivity Context
A very useful pattern in complex applications and creating a reactive "context" that is shared between components in a hierarchy:
// 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);
}
Conclusions and Next Steps
$state e $derived they are the two pillars of reactivity in Svelte 5. Theirs
ability to function outside of .svelte components opens up architectural possibilities that Svelte 4
did not allow: modular state management, logic testable in isolation, OOP patterns with reactivity
integrated, and sharing state between components without explicit Context APIs.
The next article examines $effect — the Rune for side effects — and explains when
use it correctly and when you should use it instead $derived. It's a crucial topic because
the abuse of $effect and the main anti-pattern in Svelte 5.
Series: Svelte 5 and Frontend Compiler-Driven
- Article 1: Compiler-Driven Approach and Mental Model
- Article 2 (this): $state and $derived — Universal Reactivity with Runes
- Article 3: $effect and the Lifecycle — When to Use It (and When Not)
- Article 4: SvelteKit SSR, Streaming and Load Functions
- Article 5: Transitions and Animations in Svelte 5
- Article 6: Accessibility in Svelte: Compiler Warnings and Best Practices
- Article 7: Global State Management: Context, Runes and Stores
- Article 8: Migrating from Svelte 4 to Svelte 5 — Practical Guide
- Article 9: Testing in Svelte 5: Vitest, Testing Library and Playwright







