Wer Svelte 4 kennt, findet in Svelte 5 einige neue Konzepte. Wer Svelte 5 lernt und auf alten Code stößt, findet umgekehrt unbekannte Syntax: nackte let-Variablen, $:-Label, export let. Dieser Artikel ordnet das Legacy-Reactivity-Modell ein, erklärt, was Svelte 4 genau gemacht hat, und gibt ein Migrations-Cheatsheet zur Hand. Wichtig: Svelte 5 unterstützt Legacy-Code per Auto-Detection — bestehende Komponenten laufen weiter.
Reaktive let-Variablen
In Svelte 4 war jede Top-Level-let-Variable einer Komponente automatisch reaktiv:
<script>
let count = 0;
</script>
<button on:click={() => count++}>
{count}
</button>Der Compiler erkannte: count wird im Markup gelesen und in einem Handler verändert ⇒ wird reaktiv übersetzt. Das war kompakt, hatte aber Schwächen:
- Funktionierte nur in
.svelte-Dateien, nicht in normalen.ts-Modulen. - Implizite Reaktivität ließ sich schwer per IDE-Tooling sehen.
- Bei tief verschachtelten Mutationen brauchte man manchmal trotzdem manuelle Workarounds wie
count = count.
In Svelte 5 ersetzt $state(...) dieses Modell:
<script>
let count = $state(0);
</script>
<button onclick={() => count++}>{count}</button>Das $:-Label
Das wohl bekannteste Svelte-4-Konstrukt war das reaktive Label:
<script>
let count = 0;
// Abgeleiteter Wert
$: doubled = count * 2;
// Reaktive Anweisung (Side Effect)
$: console.log('count:', count);
// Block-Form
$: {
if (count > 5) {
console.warn('groß!');
}
}
</script>$: markierte ein Statement als „läuft, sobald sich eine darin verwendete reaktive Variable ändert”. Der Compiler verfolgte die Abhängigkeiten automatisch.
Probleme:
- Ableitung und Side Effect wurden mit derselben Syntax ausgedrückt — nicht klar trennbar.
- Reihenfolge der
$:-Statements im Code spielte eine Rolle (topologische Sortierung). - Komplexere Logik im Block führte zu schwer lesbarem Code.
In Svelte 5 wird das aufgespalten:
<script>
let count = $state(0);
// Ableitung
let doubled = $derived(count * 2);
// Side Effect
$effect(() => console.log('count:', count));
// Block-Form
$effect(() => {
if (count > 5) console.warn('groß!');
});
</script>Props mit export let
In Svelte 4 war export let der Mechanismus für Komponenten-Props:
<script>
export let name;
export let age = 30; // Default
export let title = 'Dr.'; // Default
</script>
<h1>{title} {name}, {age}</h1>Eigenheiten:
- Jede Prop war ein einzelnes Statement.
- Bind:value war auf jeder Prop möglich (alle waren implizit bindbar).
- Es gab
$$propsund$$restPropsfür unbenannte Props.
In Svelte 5 ersetzt $props() mit Destrukturierung:
<script>
let { name, age = 30, title = 'Dr.', ...rest } = $props();
</script>Two-Way-Binding wird explizit über $bindable(...) gewünscht, nicht mehr automatisch.
beforeUpdate und afterUpdate
Svelte 4 hatte Lifecycle-Hooks, die rund um DOM-Updates liefen:
<script>
import { beforeUpdate, afterUpdate } from 'svelte';
beforeUpdate(() => { /* vor DOM-Update */ });
afterUpdate(() => { /* nach DOM-Update */ });
</script>In Svelte 5 deckt $effect.pre(...) (vor DOM) und $effect(...) (nach DOM) den selben Anwendungsfall ab — und mit klarer definierten Abhängigkeiten:
<script>
$effect.pre(() => { /* vor DOM-Update */ });
$effect(() => { /* nach DOM-Update */ });
</script>onMount und onDestroy existieren weiter.
Migrations-Cheatsheet
| Bereich | Svelte 4 | Svelte 5 |
|---|---|---|
| Reaktive Variable | let count = 0; | let count = $state(0); |
| Abgeleiteter Wert | $: doubled = count * 2; | let doubled = $derived(count * 2); |
| Side Effect | $: console.log(count); | $effect(() => console.log(count)); |
| Pre-DOM-Update | beforeUpdate(() => …) | $effect.pre(() => …) |
| Post-DOM-Update | afterUpdate(() => …) | $effect(() => …) |
| Prop empfangen | export let name; | let { name } = $props(); |
| Prop mit Default | export let name = 'Welt'; | let { name = 'Welt' } = $props(); |
| Required Prop | export let name; (kein Default) | let { name }: Props = $props(); (Type required) |
| Bindable Prop | implizit bei jeder export let | explizit $bindable(...) |
$$props | let allProps = $$props; | let { ...rest } = $props(); |
| Rest-Props | $$restProps | let { ...rest } = $props(); |
| DOM-Event | on:click={handle} | onclick={handle} |
| Event-Modifier | `on:click | preventDefault` |
| Component-Event | createEventDispatcher | Callback-Prop |
| Slot | <slot />, <slot name="..."> | Snippet via children / Snippet-Props |
| Slot-Argument | <slot {value} /> | Snippet-Argument |
| Mount | new App({ target }) | mount(App, { target }) |
Auto-Detection – beides läuft
Svelte 5 erkennt anhand der verwendeten Syntax automatisch, ob eine Komponente im Legacy-Modus oder im Runes-Modus läuft. Das heißt:
- Eine bestehende Svelte-4-Komponente (
export let,$:) läuft unverändert weiter. - Eine neue Komponente mit
$state(...)läuft im Runes-Modus. - Beide können in derselben App nebeneinander existieren.
Wer migrieren will, macht das schrittweise — Datei für Datei. Das offizielle Tool dafür ist:
npx sv migrate svelte-5Es kümmert sich um den Großteil der mechanischen Umstellung (Reactivity, Events, Props). Komplexere Fälle (Slots -> Snippets, createEventDispatcher -> Callback-Props) bleiben Handarbeit.
Wann jetzt migrieren?
- Aktiv weiterentwickelte Projekte: schrittweise auf Svelte 5 umstellen, neue Komponenten direkt in Runes schreiben.
- Stabiler Bestandscode mit wenig Änderung: Legacy-Modus belassen, solange keine neuen Features nötig sind.
- Gemischte Codebase: kein Problem — beide Modi koexistieren in derselben App.
Auf lange Sicht wird das Legacy-Reactivity-System nicht weiterentwickelt; neue Features (z. B. useSyncExternalStore-Äquivalente, neue $effect-Helper) gibt es nur in Runes.