Vor Svelte 5 war die Antwort einfach: Geteilten State zwischen Komponenten = Store. Mit Runes hat sich das geändert. $state in einer .svelte.ts-Datei ist heute oft die einfachere und ergonomischere Wahl. Stores sind aber nicht weg — sie haben ihre Stärken weiterhin. Dieser Artikel hilft dir, in jeder Situation die richtige Wahl zu treffen, mit drei realen Szenarien als Vergleich.
Die kurze Antwort
| Situation | Empfehlung |
|---|---|
| Geteilter Wert zwischen wenigen Komponenten in einer App | $state |
| Lokaler State innerhalb einer Komponente | $state |
| Modell mit Daten und Methoden (Klasse, Logik gebündelt) | $state |
Externe Bibliothek-API mit subscribe veröffentlichen | Store |
| Asynchroner Stream / Datenquelle mit klarer Lifecycle-Phase | Store (readable mit Setup-Funktion) |
| Integration mit RxJS, Solid, externen Frameworks | Store (Vertrag) |
Persistente Werte (localStorage-Sync) | Beides möglich, Custom Store oft kompakter |
In neuen Svelte-5-Projekten wirst du deutlich häufiger $state sehen als Stores. Das ist Absicht — Runes sind ergonomischer und type-safer.
Szenario 1 – Globaler Counter
Klassisches Beispiel: Zwei Komponenten, ein gemeinsamer Counter.
Mit Store
import { writable } from 'svelte/store';
export const count = writable(0);<script>
import { count } from '$lib/stores/counter';
</script>
<p>{$count}</p>
<button onclick={() => $count++}>+1</button>Mit $state
export const counter = $state({ value: 0 });<script>
import { counter } from '$lib/state/counter.svelte';
</script>
<p>{counter.value}</p>
<button onclick={() => counter.value++}>+1</button>Beide Lösungen funktionieren identisch. Die $state-Version ist etwas direkter — keine $-Syntax, kein Importieren aus svelte/store, der Wert ist ein normales Property.
Warum nicht direkt eine primitive Variable mit $state? Weil primitiv exportierte $state-Werte in TypeScript schlecht funktionieren — der reaktive „Trick” steckt im Proxy, und ein primitiver Wert hat keinen Proxy. Daher: in Objekt verpacken oder als Klasse modellieren.
Szenario 2 – Domänen-Modell mit Logik
Ein Cart, der Items, Total und Methoden zum Hinzufügen/Entfernen anbietet.
Mit Custom Store
import { writable, derived } from 'svelte/store';
function createCart() {
const items = writable<CartItem[]>([]);
return {
items: { subscribe: items.subscribe },
total: derived(items, ($items) =>
$items.reduce((sum, i) => sum + i.price * i.qty, 0)
),
add: (item: CartItem) =>
items.update((cur) => [...cur, item]),
remove: (id: string) =>
items.update((cur) => cur.filter((i) => i.id !== id)),
clear: () => items.set([]),
};
}
export const cart = createCart();Mit Klasse + $state
type CartItem = { id: string; price: number; qty: number };
class Cart {
items = $state<CartItem[]>([]);
total = $derived(
this.items.reduce((sum, i) => sum + i.price * i.qty, 0)
);
add(item: CartItem) {
this.items.push(item);
}
remove(id: string) {
this.items = this.items.filter((i) => i.id !== id);
}
clear() {
this.items = [];
}
}
export const cart = new Cart();<script>
import { cart } from '$lib/state/cart.svelte';
</script>
<ul>
{#each cart.items as item (item.id)}
<li>{item.price * item.qty} €</li>
{/each}
</ul>
<p>Gesamt: {cart.total.toFixed(2)} €</p>
<button onclick={cart.clear}>Leeren</button>Die Klassen-Variante mit $state ist deutlich kompakter und nutzt natürliche Klassen-Methoden statt manueller update-Aufrufe. Der Compiler verfolgt automatisch, was reaktiv ist — mit voller Type-Inference.
Das ist der klare Sweetspot von $state: Domänen-Modelle mit Daten und Logik in einem Objekt.
Szenario 3 – Externe Datenquelle mit Subscription
Ein WebSocket-Stream oder eine Browser-API, die kontinuierlich neue Werte liefert.
Mit Store (passt natürlich)
import { readable } from 'svelte/store';
export const isOnline = readable(navigator.onLine, (set) => {
const handle = () => set(navigator.onLine);
window.addEventListener('online', handle);
window.addEventListener('offline', handle);
return () => {
window.removeEventListener('online', handle);
window.removeEventListener('offline', handle);
};
});readable mit Setup/Cleanup ist hier genau das Richtige. Die Subscription wird automatisch aktiviert, wenn die erste Komponente abonniert, und beendet, wenn die letzte verschwindet.
Mit $state (möglich, aber umständlicher)
import { browser } from '$app/environment';
export const isOnline = $state({ value: browser ? navigator.onLine : true });
if (browser) {
const handle = () => (isOnline.value = navigator.onLine);
window.addEventListener('online', handle);
window.addEventListener('offline', handle);
// Kein automatisches Cleanup — Listener läuft bis zum Page-Unload
}Funktioniert — aber der Listener läuft bei jedem Modul-Import los und wird nie sauber entfernt. Für eine simple Online-Anzeige akzeptabel; für etwas Schwergewichtigeres wie WebSocket-Pings unschön. Hier ist readable mit der eingebauten Lifecycle-Funktion klar überlegen.
Was passiert, wenn ich beide kombiniere?
Du kannst sie mischen. Ein Store kann intern $state benutzen — und ein $state-Wert kann in einem derived-Store landen. Hier ist eine kompakte Brücke:
import { readable, type Readable } from 'svelte/store';
function toStore<T>(getValue: () => T): Readable<T> {
return {
subscribe(callback) {
let cleanup;
cleanup = $effect.root(() => {
$effect(() => {
callback(getValue());
});
});
return cleanup;
},
};
}
export const counter = $state({ value: 0 });
// Als Store-API exportieren (z. B. für eine Library)
export const counterStore = toStore(() => counter.value);In den meisten Apps brauchst du das nicht — aber wer eine Bibliothek schreibt, kann interne Runes-basierte Logik nach außen als Store anbieten und so beide Welten bedienen.
Was sich an Reaktivitäts-Verhalten unterscheidet
Ein technischer Aspekt, den man kennen sollte:
- Stores liefern bei jeder Änderung den kompletten neuen Wert ans Subscribe — auch wenn nur eine kleine Property davon anders ist.
$state-Proxys verfolgen Zugriffe feingranular: Wenn ein Effect nurcart.totalliest undcart.itemsmutiert wird, läuft der Effect nur, fallscart.totalbetroffen ist.
In großen Datenstrukturen ist das ein spürbarer Performance-Unterschied zugunsten von $state. Stores sind hier nicht „falsch”, aber bei tausenden Items und vielen Subscribern oft langsamer.
Praktische Empfehlung für neue Projekte
- Default für geteilten State:
$statein einer.svelte.ts-Datei, gerne in eine Klasse oder ein Objekt verpackt. - Wenn dieselbe Logik mehrfach nötig ist:
$state-Klassen-Pattern, eine Klasse mit Methoden. - Für externe Datenquellen mit klarem Lifecycle:
readableaussvelte/store. - Für externe Bibliotheks-API: Store-Vertrag (
subscribe-Methode), damit Nicht-Svelte-Code damit umgehen kann. - Bestehender Code mit Stores: Funktioniert weiter — kein Grund zur Massen-Migration.
Häufige Stolperfallen
$state außerhalb von .svelte.ts exportieren.
Geht nicht. $state darf nur in .svelte-, .svelte.ts- oder .svelte.js-Dateien stehen. Wer in normalem .ts arbeitet, muss zu Stores greifen.
$state mit primitivem Export.
export let count = $state(0);Beim Import in einer anderen Datei wird der aktuelle Wert kopiert — nicht der reaktive Container. Stattdessen Objekt oder Klasse exportieren.
Stores in Klassen mischen.
Funktioniert, aber meistens unnötig. Wenn du eine Klasse hast, ist $state als Property die natürlichere Wahl.
Erwarten, dass Stores nach Migration zu Runes weiter funktionieren. Tun sie. Stores sind in Svelte 5 nicht entfernt. Aber neuer Code ist meistens schöner mit Runes.