$state a $derived: Univerzální reaktivita s runami Svelte 5
Systém odezvy je srdcem každého moderního rámce uživatelského rozhraní. Svelte 5 je kompletně přepracován
jak funguje reaktivita, přechod od „kouzelného zadání“ Svelte 4 k Runy: funkce
speciální s předponou $ které komunikují přímo s kompilátorem. Dvě základní runy
jsem $state e $derived, které pokrývají 90 % případů využití odezvy v
jakákoli aplikace Svelte.
Tato příručka se ponoří do toho, jak tyto dvě runy interně fungují, proč používají ES6 Proxy
hluboká reaktivita, jako $derived implementuje automatické ukládání do paměti a — co víc
důležité — jak je lze použít mimo soubory .svelte v běžných modulech TypeScript, což umožňuje
zcela nové vzorce řízení státu.
Předpoklady
- Základní znalost Svelte 5 (viz článek 1: Přístup řízený kompilátorem)
- Středně pokročilý TypeScript (generika, proxy, getter/setter)
- Koncepce memoizace a sledování závislostí
$state: Reaktivní stav s ES6 Proxy
V Svelte 4 jakákoli proměnná deklarovaná v bloku <script> bylo to automaticky
reaktivní: kompilátor sledoval každé přiřazení a vygeneroval aktualizační kód. Toto fungovalo pro
primitivní typy, ale pro objekty a pole to mělo omezení: "hluboké" změny jako
array.push() o obj.nested.prop = value nebyli sledováni.
Svelte 5 to řeší pomocí $state, kterou používá ES6 proxy zachytit každého
přístup k objektům a jejich modifikace, včetně vnořených. Výsledkem je skutečná hluboká reaktivita:
<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>
Se Svelte 4, user.address.city = 'Milano' nespustilo by to aktualizaci uživatelského rozhraní
protože kompilátor nakreslil přiřazení pouze k proměnné nejvyšší úrovně (user = ...).
S Svelte 5 a Proxy je zachycen jakýkoli přístup v jakékoli úrovni hloubky.
Jak $state Proxy funguje interně
Abyste pochopili hlubokou odezvu, musíte pochopit, jak ES6 Proxy funguje a jak jej Svelte používá:
// 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;
}
});
}
V podstatě když píšeš user.address.city = 'Milano', stane se toto:
- Externí proxy zachycuje
.addressa vrátí vnořený proxy proaddress - Vnořený proxy zachycuje
.city = 'Milano' - Aktualizujte hodnotu a informujte systém odezvy Svelte
- Svelte naplánuje aktualizaci pouze pro vazby, které závisí na
user.address.city
Když proxy NEFUNGUJE
Existují případy, kdy odezva přes proxy nefunguje podle očekávání:
-
Destrukce ztrácí schopnost reagovat:
const { city } = user.address;—citya nyní primitivní řetězec, není proxy. Vždy používejteuser.address.citypřímo v šabloně. -
Spread ztrácí reaktivitu:
const copy = { ...user };vytvoří nereaktivní kopii. -
Map and Set vyžadují $state.raw pro vlastní pole:
Ne všechny metody exotických kolekcí jsou záplatované. Pro Map/Set použijte
$state(new Map())— Quick 5 patch i tyto.
$state with TypeScript: Full Typeing
$state a plně integrován s TypeScript. Typ je automaticky odvozen
od počáteční hodnoty, ale můžete být explicitní s generiky:
<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: Automatické úložiště
$derived vytváří vypočítané hodnoty, které jsou přepočítány pouze tehdy, když se změní jejich závislosti.
A ekvivalent useMemo Reactu, ale bez nutnosti explicitně deklarovat závislosti —
kompilátor je automaticky sleduje analýzou kódu funkce.
<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 používá a push-pull systém pro zapamatování: když se změní závislost,
$derived je označena jako "špinavá" (push), ale hodnota se okamžitě nepřepočítá.
Přepočítá se to až když si to někdo přečte (vytáhne). Pokud odvozené nikdo nečte, výpočet neproběhne.
$derived.by: Komplexní výpočty s funkcí
Pro výpočty vyžadující více než jeden výraz použijte $derived.by() kterou přijímá
funkce s tělem:
<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>
Runy v souborech TypeScript: Revoluční zprávy
Nejdůležitějším rozdílem mezi Svelte 4 a Svelte 5 není syntaxe run, ale skutečnost, že Runy fungují v jakémkoli souboru s příponou .svelte.ts nebo .svelte.js, nejen to v komponentách. To umožňuje zcela nové vzorce řízení státu.
// 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();
Tento obchod lze importovat do libovolné komponenty .svelte a používat přímo s plnou reaktivita:
<!-- 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 ve třídách TypeScript
Runy fungují také ve třídách TypeScript a umožňují vzor ve stylu OOP pro správu stavu:
// 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();
Srovnání s React useMemo a Vue vypočítané
Automatické sledování závislostí $derived eliminuje hlavní zdroj chyb
v ekvivalentních řešeních React a 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)
);
Praktický rozdíl: s Reactem zapomeňte na závislost v poli useMemo vyrábí
tiché zastaralé hodnoty, notoricky obtížné ladění chyby. Svelte 5 tento problém nemá
protože ke sledování dochází za běhu prostřednictvím proxy.
Kdy použít $state vs $derived
- Použijte $state pro hodnoty, které se mění v reakci na akce nebo události uživatele (vstupní pole, přepínače, data načtená z API)
- Použijte $derived pro cokoli, co lze vypočítat z existujícího stavu $ (součty, filtry, formátování, transformace)
-
Pro výpočty nepoužívejte $efekt — pokud se přistihnete při psaní
$effect(() => { derived = compute(state); }), USA$derivedMísto toho
Pokročilý vzor: Sdílený kontext reaktivity
Velmi užitečný vzor ve složitých aplikacích a vytváření reaktivního „kontextu“, který je sdílen mezi komponenty v hierarchii:
// 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);
}
Závěry a další kroky
$state e $derived jsou to dva pilíře reaktivity ve Svelte 5. Jejich
schopnost fungovat mimo komponenty .svelte otevírá architektonické možnosti, které Svelte 4
neumožňoval: modulární řízení stavu, logiku testovatelnou izolovaně, OOP vzory s reaktivitou
integrované a sdílení stavu mezi komponentami bez explicitních kontextových API.
Další článek zkoumá $efekt — Runa pro vedlejší účinky — a vysvětluje, kdy
používejte jej správně a kdy byste jej měli použít $derived. Je to zásadní téma, protože
zneužívání $effect a hlavní anti-vzor v Svelte 5.
Řada: Svelte 5 a Frontend Compiler-Driven
- Článek 1: Přístup řízený kompilátorem a mentální model
- Článek 2 (tento): $state a $derived — Univerzální reaktivita s runami
- Článek 3: $efekt a životní cyklus – kdy jej použít (a kdy ne)
- Článek 4: SvelteKit SSR, funkce streamování a načítání
- Článek 5: Přechody a animace ve Svelte 5
- Článek 6: Přístupnost ve Svelte: Varování a osvědčené postupy kompilátoru
- Článek 7: Globální správa státu: kontext, runy a obchody
- Článek 8: Přechod ze Svelte 4 na Svelte 5 – Praktický průvodce
- Článek 9: Testování ve Svelte 5: Vitest, testovací knihovna a dramatik







