$stan i $pochodna: Uniwersalna reaktywność z Svelte 5 Runami
System responsywności jest sercem każdego nowoczesnego frameworka interfejsu użytkownika. Svelte 5 został całkowicie przeprojektowany
jak działa reaktywność, przechodząc od „magicznego zadania” Svelte 4 do Runy: funkcje
specjalne z przedrostkiem $ które komunikują się bezpośrednio z kompilatorem. Dwie podstawowe Runy
jestem $state e $derived, które obejmują 90% przypadków użycia responsywności w
dowolną aplikację Svelte.
W tym przewodniku szczegółowo opisano, jak te dwie runy działają wewnętrznie i dlaczego korzystają z serwera proxy ES6
głęboka reaktywność, np $derived implementuje automatyczne zapamiętywanie i — co więcej
ważne — jak można ich używać poza plikami .svelte w zwykłych modułach TypeScript, umożliwiając
zupełnie nowe wzorce zarządzania państwem.
Warunki wstępne
- Podstawowa znajomość Svelte 5 (patrz artykuł 1: Podejście oparte na kompilatorze)
- Średnio zaawansowany TypeScript (generyczny, proxy, getter/setter)
- Koncepcja zapamiętywania i śledzenia zależności
$state: Stan reaktywny z serwerem proxy ES6
W Svelte 4 dowolna zmienna zadeklarowana w bloku <script> to było automatycznie
reaktywne: kompilator śledził każde przypisanie i wygenerował kod aktualizacji. To zadziałało
typy pierwotne, ale w przypadku obiektów i tablic miał ograniczenia: „głębokie” zmiany, takie jak
array.push() o obj.nested.prop = value nie byli śledzeni.
Svelte 5 rozwiązuje ten problem za pomocą $state, z którego korzysta Serwer proxy ES6 przechwycić każdy
dostęp i modyfikacja obiektów, w tym zagnieżdżonych. Rezultatem jest prawdziwie głęboka reaktywność:
<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>
Ze Svelte 4, user.address.city = 'Milano' nie spowodowałoby to aktualizacji interfejsu użytkownika
ponieważ kompilator narysował przypisania tylko do zmiennej najwyższego poziomu (user = ...).
Dzięki Svelte 5 i Proxy każdy dostęp na dowolnym poziomie głębokości jest przechwytywany.
Jak serwer proxy $state działa wewnętrznie
Aby zrozumieć głęboką responsywność, musisz zrozumieć, jak działa ES6 Proxy i jak Svelte z niego korzysta:
// 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;
}
});
}
Zasadniczo, kiedy piszesz user.address.city = 'Milano', dzieje się tak:
- Zewnętrzny serwer proxy przechwytuje
.addressi zwraca zagnieżdżony serwer proxy dlaaddress - Zagnieżdżone przechwyty proxy
.city = 'Milano' - Zaktualizuj wartość i powiadom system reagowania Svelte
- Svelte planuje aktualizację tylko dla powiązań zależnych od
user.address.city
Kiedy serwer proxy nie działa
Istnieją przypadki, w których reakcja za pośrednictwem serwera proxy nie działa zgodnie z oczekiwaniami:
-
Destrukturyzacja traci responsywność:
const { city } = user.address;—citya teraz prymitywny ciąg znaków, nie pełnomocnik. Zawsze używajuser.address.citybezpośrednio w szablonie. -
Spread traci reaktywność:
const copy = { ...user };tworzy niereaktywną kopię. -
Map i Set wymagają $state.raw dla niestandardowych tablic:
Nie wszystkie metody kolekcji egzotycznych są załatane. W przypadku mapy/zestawu użyj
$state(new Map())— Szybka łatka 5 także i te.
$state z TypeScript: Pełne pisanie
$state i w pełni zintegrowany z TypeScript. Typ jest ustalany automatycznie
od wartości początkowej, ale możesz wyraźnie określić, używając typów ogólnych:
<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>
$pochodne: Automatyczne przechowywanie
$derived tworzy obliczone wartości, które są przeliczane ponownie tylko wtedy, gdy zmienią się ich zależności.
I odpowiednik useMemo Reacta, ale bez konieczności jawnego deklarowania zależności —
kompilator automatycznie je śledzi, analizując kod funkcji.
<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 wykorzystuje a systemem push-pull do zapamiętywania: kiedy zmienia się zależność,
$derived jest oznaczony jako „brudny” (push), ale wartość nie jest natychmiast przeliczana.
Jest on przeliczany dopiero wtedy, gdy ktoś go przeczyta (pull). Jeżeli nikt nie czyta wyprowadzonych obliczeń, obliczenia nie mają miejsca.
$derived.by: Złożone obliczenia z funkcją
W przypadku obliczeń wymagających więcej niż jednego wyrażenia użyj $derived.by() które on akceptuje
funkcja z ciałem:
<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 w plikach TypeScript: rewolucyjne wiadomości
Najważniejszą różnicą pomiędzy Svelte 4 i Svelte 5 nie jest składnia Run, ale fakt, że Runy działają w dowolnym pliku z rozszerzeniem .svelte.ts lub .svelte.js, nie tylko to w komponentach. Umożliwia to zupełnie nowe wzorce zarządzania państwem.
// 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();
Ten sklep można zaimportować do dowolnego komponentu .svelte i używać bezpośrednio, w całości reaktywność:
<!-- 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 w klasach TypeScript
Runy działają również w klasach TypeScriptu, umożliwiając wzorzec w stylu OOP do zarządzania stanem:
// 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();
Porównanie z Reactem, obliczono useMemo i Vue
Automatyczne śledzenie zależności $derived eliminuje główne źródło błędów
w równoważnych rozwiązaniach 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)
);
Praktyczna różnica: w React zapomnij o zależności w tablicy useMemo produkuje
ciche, przestarzałe wartości, notorycznie trudny błąd do debugowania. W Svelte 5 nie ma tego problemu
ponieważ śledzenie odbywa się w czasie wykonywania za pośrednictwem serwera proxy.
Kiedy używać stanu $ zamiast wartości pochodnej
- Użyj $stanu dla wartości, które zmieniają się w odpowiedzi na działania lub zdarzenia użytkownika (pola wejściowe, przełączniki, dane ładowane z API)
- Użyj $pochodnego dla wszystkiego, co można obliczyć na podstawie istniejącego stanu $ (sumy, filtry, formatowanie, przekształcenia)
-
Nie używaj $effect do obliczeń – jeśli stwierdzisz, że piszesz
$effect(() => { derived = compute(state); }), Stany Zjednoczone$derivedZamiast
Wzorzec zaawansowany: wspólny kontekst reaktywności
Bardzo przydatny wzorzec w złożonych aplikacjach i tworzeniu reaktywnego „kontekstu”, który jest współdzielony pomiędzy komponentami w 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);
}
Wnioski i dalsze kroki
$state e $derived to dwa filary reaktywności w Svelte 5. Ich
zdolność do funkcjonowania poza smukłymi komponentami otwiera możliwości architektoniczne, jakie oferuje Svelte 4
nie pozwalało na: modułowe zarządzanie stanem, logikę testowalną w izolacji, wzorce OOP z reaktywnością
zintegrowane i współdzielące stan pomiędzy komponentami bez jawnych kontekstowych interfejsów API.
Następny artykuł analizuje $efekt — Runa skutków ubocznych — i wyjaśnia, kiedy
używaj go poprawnie i kiedy powinieneś go używać zamiast tego $derived. To ważny temat, ponieważ
nadużycie $effect i główny anty-wzór w Svelte 5.
Seria: Svelte 5 i Frontend sterowane kompilatorem
- Artykuł 1: Podejście oparte na kompilatorze i model mentalny
- Artykuł 2 (ten): $state i $derived — Uniwersalna reaktywność z runami
- Artykuł 3: $effect i cykl życia — kiedy go używać (a kiedy nie)
- Artykuł 4: SvelteKit SSR, funkcje przesyłania strumieniowego i ładowania
- Artykuł 5: Przejścia i animacje w Svelte 5
- Artykuł 6: Dostępność w Svelte: Ostrzeżenia kompilatora i najlepsze praktyki
- Artykuł 7: Globalne zarządzanie stanem: kontekst, runy i sklepy
- Artykuł 8: Migracja ze Svelte 4 do Svelte 5 – Poradnik praktyczny
- Artykuł 9: Testowanie w Svelte 5: Vitest, biblioteka testowa i dramaturg







