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:

svelte Svelte 4 – implizite Reaktivität
<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:

svelte Svelte 5 – explizit
<script>
    let count = $state(0);
</script>

<button onclick={() => count++}>{count}</button>

Das $:-Label

Das wohl bekannteste Svelte-4-Konstrukt war das reaktive Label:

svelte Svelte 4 – $:-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:

svelte Svelte 5 – getrennt
<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:

svelte Svelte 4 – 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 $$props und $$restProps für unbenannte Props.

In Svelte 5 ersetzt $props() mit Destrukturierung:

svelte Svelte 5 – Props
<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:

svelte Svelte 4 – Update-Hooks
<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:

svelte Svelte 5
<script>
    $effect.pre(() => { /* vor DOM-Update */ });
    $effect(() => { /* nach DOM-Update */ });
</script>

onMount und onDestroy existieren weiter.

Migrations-Cheatsheet

BereichSvelte 4Svelte 5
Reaktive Variablelet 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-UpdatebeforeUpdate(() => …)$effect.pre(() => …)
Post-DOM-UpdateafterUpdate(() => …)$effect(() => …)
Prop empfangenexport let name;let { name } = $props();
Prop mit Defaultexport let name = 'Welt';let { name = 'Welt' } = $props();
Required Propexport let name; (kein Default)let { name }: Props = $props(); (Type required)
Bindable Propimplizit bei jeder export letexplizit $bindable(...)
$$propslet allProps = $$props;let { ...rest } = $props();
Rest-Props$$restPropslet { ...rest } = $props();
DOM-Eventon:click={handle}onclick={handle}
Event-Modifier`on:clickpreventDefault`
Component-EventcreateEventDispatcherCallback-Prop
Slot<slot />, <slot name="...">Snippet via children / Snippet-Props
Slot-Argument<slot {value} />Snippet-Argument
Mountnew 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:

bash Migrations-Tool
npx sv migrate svelte-5

Es 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.

Weiterführende Ressourcen

Externe Quellen

Verwandte Artikel

/ Weiter

Zurück zu Reactivity (Runes)

Zur Übersicht