Universeller State (.svelte.js / .svelte.ts)
Geteilter, reaktiver Zustand ohne Stores: Runes leben in einem Modul und werden von beliebig vielen Komponenten geteilt.
Runes außerhalb von Komponenten
$state & Co. funktionieren nicht nur in .svelte-Dateien. Wenn du eine
Datei auf .svelte.js oder .svelte.ts endest, verarbeitet der
Svelte-Compiler auch dort Runes. Damit kannst du reaktiven Zustand in ein ganz normales Modul
legen — und jede Komponente, die dieses Modul importiert, teilt dieselbe Instanz dieses Zustands. Das ist Svelte-5-„universeller State": komponentenübergreifend,
aber ohne die Store-API.
ℹ️ Die Datei-Endung ist entscheidend
Nur in .svelte.js/.svelte.ts-Dateien werden Runes compiliert. Eine
gewöhnliche .ts-Datei mit $state führt zu einem Fehler.
Die Reexport-Falle
Es liegt nahe, einfach export let count = $state(0) zu schreiben — das
funktioniert aber nicht wie erhofft. Beim Import würde nur der momentane Wert kopiert, nicht die reaktive Bindung. Andere Module sähen spätere Änderungen nie.
// ❌ FALSCH: Rune direkt als Top-Level-Variable exportieren
export let count = $state(0);
// Beim Import wird der WERT kopiert, nicht die reaktive Bindung.
// Andere Module sehen Änderungen nicht — die "Reexport-Falle".Die Lösung: den Zustand in ein Objekt mit Gettern (oder Funktionen) kapseln. Der Getter liest den reaktiven Wert bei jedem Zugriff frisch aus — so bleibt die Reaktivität über die Modulgrenze hinweg erhalten.
// counter.svelte.ts — Datei-Endung .svelte.ts aktiviert Runes!
let count = $state(0);
export const counter = {
get value() {
return count; // Getter liest den reaktiven Zustand LIVE
},
increment() {
count += 1;
},
reset() {
count = 0;
}
};<script>
import { counter } from '$lib/counter.svelte.js';
</script>
<!-- Jede Komponente, die das Modul importiert, teilt denselben Zustand -->
<button onclick={() => counter.increment()}>
Geklickt: {counter.value}
</button>⚠️ Warum ein Getter, kein einfaches Feld?
Würdest du { value: count } zurückgeben, wäre value ein
eingefrorener Schnappschuss. Ein get value() hingegen wird bei jedem Lesen neu
ausgewertet und meldet dem Compiler die Abhängigkeit — genau das hält die UI reaktiv.
Das echte Beispiel: theme.svelte.ts
Genau dieses Muster steckt in diesem Kurs. Der Theme-Umschalter oben rechts (Hell/Dunkel)
wird von src/lib/theme.svelte.ts angetrieben — einem Runes-Modul mit exakt der
Getter-Kapselung von oben:
// src/lib/theme.svelte.ts — echtes Beispiel aus DIESEM Kurs
import { browser } from '$app/environment';
function createTheme() {
let dark = $state(true); // modulweiter reaktiver Zustand
if (browser) {
const stored = localStorage.getItem('theme');
dark = stored ? stored === 'dark'
: document.documentElement.classList.contains('dark');
}
function apply() {
if (!browser) return;
document.documentElement.classList.toggle('dark', dark);
localStorage.setItem('theme', dark ? 'dark' : 'light');
}
return {
get dark() { // Getter: gibt den LIVE-Wert zurück, nicht eine Kopie
return dark;
},
toggle() {
dark = !dark;
apply();
}
};
}
// Eine einzige Instanz für die ganze App — der geteilte Zustand:
export const theme = createTheme();Die letzte Zeile, export const theme = createTheme(), erzeugt genau eine Instanz. Jede Komponente, die theme importiert — der ThemeToggle im Header, ein Setting-Panel, was auch immer — greift auf denselben dark-Zustand zu. Ein Klick auf theme.toggle() ändert dark, und jede Stelle, die theme.dark liest, aktualisiert sich
sofort:
<script>
import { theme } from '$lib/theme.svelte';
</script>
<button onclick={() => theme.toggle()}>
Modus: {theme.dark ? '🌙 Dunkel' : '☀️ Hell'}
</button>💡 Genau das erlebst du gerade
Der Umschalter in der Kopfzeile dieser Seite nutzt dieses Modul. Kein Store, kein Context —
nur ein importiertes Objekt mit einem $state darin. Ein einziger Ort für den
Zustand, den die ganze App teilt.
Live ausprobieren
Zur Abrundung ein kleiner lokaler Zähler in dieser Seite. Er nutzt dasselbe Getter-Prinzip
nicht (er ist komponentenlokal) — verlagerst du sein $state aber in ein .svelte.ts-Modul, wäre er sofort app-weit teilbar:
Dieser Wert lebt nur in dieser Komponente. Im Modul wäre er global geteilt.
Einordnung zu Rails
🛤️ Rails-Vergleich: global sichtbar, aber reaktiv
Ein prozessweiter globaler Wert in Rails ist etwa eine Konstante oder ein Redis-Eintrag —
überall sichtbar, aber statisch pro Request. Ein .svelte.ts-Modul ist das
clientseitige Pendant: ein einziger, app-weit importierbarer Zustand — nur eben reaktiv, sodass jede Ansicht automatisch nachzieht, sobald er sich ändert.
# Rails: ein prozessweiter, global geteilter Wert wäre z.B. eine
# Konstante oder ein Redis-Eintrag — aber statisch pro Request.
module AppConfig
THEME_DEFAULT = "dark" # global sichtbar, aber NICHT reaktiv
end
# Ein .svelte.ts-Modul ist das clientseitige Gegenstück:
# global sichtbar UND reaktiv — die UI folgt jeder Änderung.Zusammenfassung
💡 Das Wichtigste
- Runes funktionieren in
.svelte.js/.svelte.ts-Modulen — dort lebt geteilter, reaktiver Zustand. - Niemals
export let x = $state(...)als Top-Level-Variable exportieren (Reexport-Falle). - Stattdessen in ein Objekt mit Gettern/Methoden kapseln — wie
theme.svelte.tsin diesem Kurs. - Für neue Projekte ist dieses Muster oft die schlankere Alternative zu Stores; Stores bleiben stark bei async/RxJS-artigen Fällen.
🎯 Probier es selbst
Lege eine Datei counter.svelte.ts mit dem Getter-Muster von oben an, importiere counter in zwei verschiedene Komponenten und zeige in beiden counter.value an. Klicke in der einen — und beobachte, wie sich der Wert in der
anderen mitbewegt.