Wer in Svelte 5 reaktiven Code schreibt, schreibt Runes. Das sind kleine Sprach-Bausteine, die mit $ beginnen und vom Compiler erkannt werden — $state, $derived, $effect, $props, $bindable, $inspect und $host. Sie ersetzen die unsichtbare Reaktivität aus Svelte 4 (let-Variablen, $:-Label, export let) durch sieben klar benannte Werkzeuge mit jeweils einer Aufgabe. Dieser Artikel zeigt, was die Runes wirklich sind, wie sie zusammenspielen und welche Regeln man im Hinterkopf behalten muss.
Drei Werkzeuge, die fast jeden Code prägen
Wenn du eine echte Anwendung mit Svelte 5 baust, kommen drei Runes immer wieder vor:
$state(initial)— du sagst: „dieser Wert kann sich ändern, andere sollen es mitbekommen.”$derived(expression)— du sagst: „dieser Wert berechnet sich aus anderen reaktiven Werten.”$effect(() => …)— du sagst: „diese Aktion soll laufen, wenn sich bestimmte Werte ändern.”
Daneben gibt es vier Helfer für spezifischere Aufgaben: $props (Komponenten-Properties auslesen), $bindable (eine Prop für Two-Way-Binding freigeben), $inspect (Debug-Logs) und $host (Web-Components). Wer die ersten drei verstanden hat, hat 80 % der täglichen Arbeit abgedeckt.
Ein vollständiges Mini-Beispiel
Bevor wir in Definitionen einsteigen, ein funktionierender Mini-Code, der alles zusammenbringt:
<script>
let count = $state(0);
let doubled = $derived(count * 2);
$effect(() => {
document.title = `Wert: ${count}`;
});
</script>
<button onclick={() => count++}>+1</button>
<p>{count} (doppelt: {doubled})</p>Drei Zeilen, drei Aufgaben:
countist die Quelle der Reaktivität. Beim Klick wird sie verändert (count++).doubledist die Folge dieser Quelle. Niemand muss sie aktualisieren — Svelte tut das, sobaldcountsich ändert.document.title = …ist eine Wirkung auf die Außenwelt. Sie läuft beim ersten Render und bei jeder weiteren Änderung der darin verwendeten Werte.
Genau dieser Dreiklang — Quelle, Folge, Wirkung — ist das mentale Modell hinter Runes. Alles andere baut darauf auf.
Übersicht aller sieben Runes
| Rune | Kurz | Wo verwendbar |
|---|---|---|
$state | Reaktiver Wert | .svelte, .svelte.js, .svelte.ts |
$derived | Aus anderen reaktiven Werten berechnet | .svelte, .svelte.js, .svelte.ts |
$effect | Reagiert auf Änderungen | Im Component-Tree (.svelte oder Setup) |
$props | Komponenten-Properties auslesen | nur in .svelte |
$bindable | Prop als zwei-weg-bindbar markieren | nur in .svelte |
$inspect | Debug-Log bei reaktiven Änderungen | überall in Runes-Dateien |
$host | Zugriff auf das Host-Element bei Custom-Elements | nur in entsprechenden .svelte-Dateien |
Importieren musst du nichts. Der Compiler erkennt die Namen.
Warum Runes „Keywords” sind und nicht „Funktionen”
Auf den ersten Blick sieht $state(0) aus wie ein Funktionsaufruf. Technisch ist es aber etwas anderes — ein Sprachkonstrukt, das der Compiler beim Übersetzen erkennt und durch echten Reaktivitäts-Code ersetzt.
Praktischer Unterschied: Du kannst Runes nicht herumreichen wie normale Funktionen.
// Keine Variable, keine Wiederverwendung
const reactive = $state;
const value = reactive(0);
// Kein Argument für eine eigene Funktion
function buildState(rune, initial) {
return rune(initial);
}
// Kein Import — der Name ist eingebaut
import { $state } from 'svelte'; // existiert nichtDer Grund ist Kontrolle: Damit der Compiler reaktiven Code erzeugen kann, muss er direkt sehen, wo eine Rune steht. Sobald sie über einen Umweg käme — eine Variable, ein Funktionsargument — wäre die statische Analyse vorbei. Die Einschränkung wirkt erst irritierend, wird aber im Alltag praktisch nie spürbar.
.svelte.ts und .svelte.js: Runes außerhalb von Komponenten
Eine wichtige Eigenschaft, die in Svelte 4 fehlte: Runes funktionieren auch außerhalb von Komponenten — vorausgesetzt, die Datei trägt eine spezielle Endung.
| Datei-Endung | Runes erlaubt? |
|---|---|
.svelte | Ja, alle |
.svelte.ts oder .svelte.js | Ja, außer $props / $bindable |
Normales .ts oder .js | Nein |
Damit lassen sich wiederverwendbare Logik-Bausteine bauen, die genauso reaktiv sind wie der Code in einer Komponente. Beispiel: ein Counter, dessen Logik aus der Komponente herausgelöst ist.
export function createCounter(initial = 0) {
let count = $state(initial);
let doubled = $derived(count * 2);
return {
get count() { return count; },
get doubled() { return doubled; },
increment() { count++; },
reset() { count = 0; },
};
}<script>
import { createCounter } from '$lib/counter.svelte';
const counter = createCounter(10);
</script>
<button onclick={counter.increment}>
{counter.count} (doppelt: {counter.doubled})
</button>Eine Sache fällt im Helfer auf — get count() statt einfach count. Der Grund: Bei einem normalen Property liest die aufrufende Komponente den Wert zum Zeitpunkt des Aufrufs. Bei einem Getter ruft sie jedes Mal neu den aktuellen Wert ab. Nur die zweite Variante bleibt reaktiv. Wer also Werte aus einer .svelte.ts-Datei nach außen gibt, packt sie typischerweise hinter einen Getter.
Wo darf welche Rune stehen?
| Datei | $state | $derived | $effect | $props | $bindable | $inspect |
|---|---|---|---|---|---|---|
.svelte | + | + | + | + | + | + |
.svelte.js / .svelte.ts | + | + | +¹ | − | − | + |
Normales .js / .ts | − | − | − | − | − | − |
¹ $effect braucht einen Component-Lifecycle, an den er sich hängen kann. In einer .svelte.ts-Datei läuft das nur, wenn die Funktion als Setup einer Komponente aufgerufen wird. Wer Effects ohne Component-Bindung braucht, greift zu $effect.root(...) — siehe Artikel zu $effect.
$props und $bindable ergeben außerhalb einer Komponente keinen Sinn — eine .svelte.ts-Datei hat keine Properties.
Wie Reaktivität wirklich „fließt”
Beim mentalen Modell hilft es, sich Quellen, Folgen und Wirkungen als gerichteten Graphen vorzustellen:
$state(a) ──┐
├──> $derived(sum) ──> $effect(log)
$state(b) ──┘In Code:
let a = $state(1);
let b = $state(2);
let sum = $derived(a + b);
$effect(() => {
console.log('sum:', sum);
});
a = 5;
// sum wird zu 7
// Effect loggt "sum: 7"Drei Punkte sind wichtig zu verstehen:
- Du musst keine Abhängigkeiten deklarieren. Anders als bei Reacts
useEffect([a, b])erkennt Svelte automatisch, welche reaktiven Werte gelesen werden — auch durch Funktionsgrenzen hindurch. - Die Auswertung ist lazy.
$derived(a + b)rechnet nichts vor, solange niemand den Wert liest. Erst beim Zugriff (im Markup oder in einem Effect) wird die Summe gebildet — und dann gecacht, bis sichaoderbändern. - Effects laufen nach dem DOM-Update, nicht synchron mit dem Setter. Wer in einem Effect den State weiter ändert, sollte das im Hinterkopf haben.
Vergleich zu Svelte 4
In Svelte 4 war Reaktivität implizit. Eine let-Variable in einer Komponente war einfach reaktiv, abgeleitete Werte schrieb man hinter ein $:-Label.
<script>
export let initial = 0;
let count = initial;
$: doubled = count * 2;
$: console.log(count);
</script>In Svelte 5 wird daraus:
<script>
import { untrack } from 'svelte';
let { initial = 0 } = $props();
let count = $state(untrack(() => initial));
let doubled = $derived(count * 2);
$effect(() => console.log(count));
</script>Was hat sich geändert?
- Reaktive Werte sind explizit markiert. Beim Lesen einer
let-Zeile in Svelte 5 weißt du sofort, ob es um normalen Code oder um Reaktivität geht. - Ableitung und Side Effect sind getrennte Werkzeuge.
$:machte beides — was bei größeren Code-Stellen schnell unübersichtlich wurde. - Die Reaktivität funktioniert auch in Helfer-Modulen (
.svelte.ts), nicht nur in Komponenten. - TypeScript profitiert deutlich: Der Compiler kennt den Typ des reaktiven Werts, IDE-Vorschläge sind treffsicherer.
Der Wechsel ist im Tagesalltag nach kurzer Eingewöhnung kaum Mehrarbeit — eher Mehrgewinn an Klarheit.
Was leicht schiefgeht
Vier Fehler, die Anfängern und Umsteigern oft passieren:
1. Reaktivität durch Destrukturierung verlieren.
let user = $state({ name: 'Anna' });
const { name } = user;
// name ist eine Konstante mit Wert 'Anna' — sie reagiert nicht mehr.Lösung: Im Markup oder in Funktionen direkt user.name schreiben, statt zu destrukturieren.
2. $state in einer normalen .ts-Datei aufrufen.
Der Compiler ignoriert das. Datei umbenennen auf .svelte.ts, dann funktioniert es.
3. $state mit primitivem Initial-Wert nach außen exportieren.
export let count = $state(0);Beim Import in einer anderen Datei wird der aktuelle Wert kopiert — nicht der reaktive Container. Lösung: in einem Objekt verpacken ({ value: 0 }) oder eine Klasse verwenden.
4. $state einsetzen, wo $derived richtig wäre.
let count = $state(0);
let doubled = $state(count * 2);
// doubled wird einmalig berechnet und bleibt 0 — auch nach count++.Berechnete Werte gehören in $derived. $state ist nur für Quellen.