HomeBlog › SvelteKit & Svelte
SvelteKit & Svelte $state $derived $effect Migration

Svelte 5 Runes mit Claude Code:
Reaktivität 2026

Svelte 5 Runes revolutionieren das Reaktivitäts-Modell — Claude Code erklärt $state, $derived, $effect und Snippets und migriert Svelte-4-Komponenten.

📅 6. Mai 2026 ⏱ 10 min Lesezeit ✍ SpockyMagicAI

Svelte 5 bringt das bisher tiefgreifendste Redesign seit dem Start des Frameworks: Runes. Was auf den ersten Blick wie „noch mehr Sigils" wirkt, ist in Wahrheit ein vollständig neues, explizites Reaktivitätssystem — weg von magischem Compiler-Zustand hin zu transparenten, kompositionsfähigen Primitiven. Dieses Tutorial zeigt anhand von Claude-Code-generierten Beispielen, wie du die sechs Kern-Runes einsetzt und bestehende Svelte-4-Komponenten Schritt für Schritt migrierst.

Voraussetzungen

Grundkenntnisse in Svelte 4 oder SvelteKit. Svelte ≥ 5.0 (npm create svelte@latest). Node ≥ 20. Claude Code als KI-Pair-Programmer im Terminal.

1 $state — Das neue Herzstück der Reaktivität

In Svelte 4 war reaktiver Zustand implizit: jede let-Variable im Skript-Block war automatisch reaktiv. In Svelte 5 ist das explizit — du musst $state() aufrufen, um reaktiven Zustand zu deklarieren. Das mag zunächst nach mehr Boilerplate klingen, schafft aber massive Klarheit für Tooling, TypeScript-Integration und Komposierbarkeit.

Grundlegende Verwendung


<script>
  // Svelte 4 (alt): let count = 0; // implizit reaktiv
  // Svelte 5 (neu): explizit mit $state()
  let count = $state(0);

  function increment() {
    count++;
  }
</script>

<button onclick={increment}>
  Klicks: {count}
</button>

Deep Reactivity — Objekte und Arrays

Ein besonderer Vorteil von $state() gegenüber einfachen Stores: verschachtelte Objekte und Arrays sind automatisch tief reaktiv. Du musst nicht mehr manuell todos = [...todos] schreiben.

<script>
  let todos = $state([
    { id: 1, text: 'Svelte 5 lernen', done: false },
    { id: 2, text: 'Claude Code einrichten', done: true  },
  ]);

  // Direkte Mutation — kein Spread nötig!
  function toggleTodo(id) {
    const todo = todos.find(t => t.id === id);
    if (todo) todo.done = !todo.done;
  }

  function addTodo(text) {
    todos.push({ id: Date.now(), text, done: false });
  }
</script>
$state.raw() — Nicht-reaktive Teilbäume

Mit $state.raw() erzeugst du einen reaktiven Zeiger auf ein Objekt, dessen innere Eigenschaften nicht beobachtet werden. Nützlich für große unveränderliche Datensätze (z. B. JSON-Config, Icon-Definitionen), bei denen du kein Deep-Proxy-Overhead möchtest. Mutationen am Objekt selbst lösen keine Updates aus — nur eine Neuzuweisung des Zeigers.

<script>
  // Nur Neuzuweisung löst Re-Render aus, Mutation der inneren Felder nicht
  let config = $state.raw({ theme: 'dark', lang: 'de' });

  function switchTheme() {
    // Erzeugt neues Objekt → Reaktivität ausgelöst
    config = { ...config, theme: config.theme === 'dark' ? 'light' : 'dark' };
  }
</script>

$state.snapshot() — Nicht-reaktive Kopie erstellen

Manchmal brauchst du einen "eingefrorenen" Snapshot des aktuellen Zustands — etwa für JSON-Serialisierung, Logging oder den Vergleich zweier Zeitpunkte. $state.snapshot() gibt dir ein einfaches POJO ohne Proxy zurück.

<script>
  let form = $state({ name: '', email: '' });

  function submit() {
    const data = $state.snapshot(form);
    // data ist jetzt ein einfaches Objekt, sicher für JSON.stringify
    fetch('/api/submit', {
      method: 'POST',
      body: JSON.stringify(data),
    });
  }
</script>
Svelte 4 Svelte 5 ($state)
let count = 0 (implizit reaktiv) let count = $state(0) (explizit)
todos = [...todos] für Array-Updates todos.push(item) funktioniert direkt
writable(0) aus svelte/store $state(0) — kein Store-Import nötig
Nicht komposierbar außerhalb von .svelte In .svelte.ts/.svelte.js Dateien nutzbar

2 $derived — Berechnete Werte ohne Redundanz

$derived() ist der Nachfolger der reaktiven Deklarationen ($: doubled = count * 2) aus Svelte 4. Der entscheidende Unterschied: $derived ist lazy und gecacht. Der Wert wird erst bei Lesezugriff neu berechnet, wenn sich eine abhängige $state-Variable geändert hat.

Einfache Ableitungen

<script>
  let count = $state(0);
  let doubled = $derived(count * 2);
  let isEven = $derived(count % 2 === 0);

  // Mehrere State-Abhängigkeiten
  let width  = $state(100);
  let height = $state(80);
  let area   = $derived(width * height);
</script>

<p>{count} × 2 = {doubled} ({isEven ? 'gerade' : 'ungerade'})</p>
<p>Fläche: {area} px²</p>

$derived.by() — Komplexe Ableitungen mit Funktionskörper

Wenn die Ableitung mehr als ein einfacher Ausdruck ist — z. B. ein Array-Filter mit mehreren Schritten — nutzt du $derived.by(). Das ist das Äquivalent zu $: { ... }-Blöcken, aber sauber als Funktion verpackt.

<script>
  let todos    = $state([...]);
  let filter   = $state('all'); // 'all' | 'active' | 'done'
  let searchTerm = $state('');

  let filteredTodos = $derived.by(() => {
    let result = todos;

    if (filter === 'active') {
      result = result.filter(t => !t.done);
    } else if (filter === 'done') {
      result = result.filter(t => t.done);
    }

    if (searchTerm.trim()) {
      const q = searchTerm.toLowerCase();
      result = result.filter(t => t.text.toLowerCase().includes(q));
    }

    return result.sort((a, b) => +a.done - +b.done);
  });

  let stats = $derived.by(() => ({
    total: todos.length,
    done:  todos.filter(t => t.done).length,
    pct:   todos.length
             ? Math.round(todos.filter(t => t.done).length / todos.length * 100)
             : 0,
  }));
</script>

<p>{stats.done}/{stats.total} erledigt ({stats.pct}%)</p>
Warum nicht einfach eine Funktion aufrufen?

Eine normale Funktion im Template ({compute()}) würde bei jedem Render erneut ausgeführt. $derived dagegen ist memoized: Die Berechnung läuft nur, wenn sich eine der genutzten $state-Quellen wirklich geändert hat. Bei teuren Berechnungen (Sortierung großer Listen, komplexe Aggregationen) macht das einen messbaren Performance-Unterschied.

3 $effect — Seiteneffekte transparent deklarieren

$effect() ersetzt onMount, afterUpdate und die reaktiven $: { sideEffect() }-Blöcke. Der wichtigste Unterschied zu React's useEffect: Svelte trackt die Abhängigkeiten automatisch — kein Dependency-Array notwendig.

<script>
  let count  = $state(0);
  let theme  = $state('dark');

  // Läuft nach jedem Render wenn count oder theme sich ändern
  $effect(() => {
    document.title = `Klicks: ${count} | Theme: ${theme}`;
  });

  // Mit Cleanup-Funktion
  $effect(() => {
    const handler = (e) => console.log('resize', e);
    window.addEventListener('resize', handler);

    // Cleanup: automatisch aufgerufen bevor Effect erneut läuft
    return () => {
      window.removeEventListener('resize', handler);
    };
  });
</script>

$effect.pre() — Vor DOM-Updates

Standardmäßig läuft $effect() nach DOM-Updates (äquivalent zu afterUpdate). $effect.pre() läuft vor dem DOM-Update — nützlich für Scroll-Position-Preservation oder Animationen, die den aktuellen DOM-Zustand lesen müssen.

<script>
  let messages = $state([]);
  let listEl;

  // Scrollposition VOR Update merken (sonst ist DOM schon verändert)
  $effect.pre(() => {
    messages; // Abhängigkeit registrieren
    if (!listEl) return;
    const atBottom =
      listEl.scrollHeight - listEl.scrollTop - listEl.clientHeight < 50;
    if (atBottom) {
      return () => {
        listEl.scrollTop = listEl.scrollHeight;
      };
    }
  });
</script>

<ul bind:this={listEl}>
  {#each messages as msg}
    <li>{msg}</li>
  {/each}
</ul>
Svelte 4 API Svelte 5 Äquivalent
onMount(() => { ... }) $effect(() => { ... })
afterUpdate(() => { ... }) $effect(() => { ... }) (nach DOM)
beforeUpdate(() => { ... }) $effect.pre(() => { ... })
onDestroy(() => cleanup()) return () => cleanup() in $effect
$: { sideEffect(value) } $effect(() => { sideEffect(value) })
Häufige Falle

$effect darf keinen State schreiben, der zugleich als Abhängigkeit gelesen wird — sonst entsteht eine Endlosschleife. Wenn du State aus einem Effect heraus setzen musst, nutze untrack(() => state = newVal) aus svelte.

4 $props und $bindable — Props mit Typsicherheit

Statt export let prop für jede einzelne Prop musst du in Svelte 5 nur noch $props() destrukturieren. Das Ergebnis: weniger Boilerplate, perfekte TypeScript-Integration und klare Unterscheidung zwischen read-only Props und zwei-Wege-Bindungen.

Einfache Props

<!-- Button.svelte -->
<script>
  // Svelte 4: export let label; export let disabled = false;
  // Svelte 5:
  let { label, disabled = false, variant = 'primary' } = $props();
</script>

<button class="btn btn-{variant}" {disabled}>
  {label}
</button>

TypeScript Props Interface

<script lang="ts">
  interface Props {
    label:    string;
    disabled?: boolean;
    variant?: 'primary' | 'secondary' | 'danger';
    onclick?: (e: MouseEvent) => void;
  }

  let {
    label,
    disabled = false,
    variant  = 'primary',
    onclick,
  }: Props = $props();
</script>

$bindable — Zwei-Wege-Bindung explizit machen

In Svelte 4 konnte der Parent jede Prop mit bind:prop binden. In Svelte 5 muss die Komponente explizit deklarieren, welche Props bindbar sind — über $bindable() als Standardwert.

<!-- CustomInput.svelte -->
<script lang="ts">
  let {
    value = $bindable(''),   // Zwei-Wege-Bindung möglich
    label,                    // Nur read-only
    placeholder = '',
  } = $props();
</script>

<label>
  {label}
  <input bind:value {placeholder} />
</label>

<!-- Verwendung im Parent -->
<script>
  let email = $state('');
</script>

<CustomInput label="E-Mail" bind:value={email} />
<p>Eingabe: {email}</p>
Rest Props (Spread Props)

Mit let { label, ...rest } = $props() fängst du alle unbekannten Props in rest auf und kannst sie via {...rest} an ein DOM-Element weitergeben — praktisch für vollständig typisierte Wrapper-Komponenten.

5 Snippets und Render Tags — Slot-Ersatz mit Superkräften

Svelte 5 ersetzt <slot> durch Snippets — benannte, parametrisierbare Template-Blöcke. Sie sind mächtiger als Slots, weil sie Parameter empfangen können und als Props weitergegeben werden dürfen.

Grundlegende Snippet-Syntax

<!-- Im Parent: Snippet definieren und an Kind übergeben -->
<script>
  import DataTable from './DataTable.svelte';
  let users = $state([...]);
</script>

<DataTable items={users}>
  {#snippet header()}
    <tr><th>Name</th><th>E-Mail</th><th>Rolle</th></tr>
  {/snippet}

  {#snippet row(user)}
    <tr>
      <td>{user.name}</td>
      <td>{user.email}</td>
      <td><span class="badge">{user.role}</span></td>
    </tr>
  {/snippet}
</DataTable>

Snippets in der Komponente empfangen

<!-- DataTable.svelte -->
<script>
  import { type Snippet } from 'svelte';

  let {
    items,
    header,  // Snippet<[]>
    row,     // Snippet<[item: typeof items[number]]>
    empty,   // optionaler Fallback-Snippet
  } = $props();
</script>

<table>
  <thead>{@render header()}</thead>
  <tbody>
    {#each items as item}
      {@render row(item)}
    {/each}
    {#if items.length === 0 && empty}
      {@render empty()}
    {/if}
  </tbody>
</table>

children-Snippet — Default Slot

<!-- Card.svelte — Default-Slot über children-Snippet -->
<script>
  import { type Snippet } from 'svelte';
  let { title, children }: { title: string; children: Snippet } = $props();
</script>

<div class="card">
  <h3>{title}</h3>
  {@render children()}
</div>

<!-- Verwendung -->
<Card title="Meine Karte">
  <p>Alles was hier steht landet in children</p>
</Card>
Snippet-Vorteile gegenüber Slots

Slots hatten einen fundamentalen Nachteil: Sie konnten keine Daten aus der Kind-Komponente nach oben reichen (abgesehen vom unhandlichen let:item-Pattern). Snippets dagegen sind einfache Funktionen mit Parametern — vollständig typisierbar und komposierbar.

6 Migration von Svelte 4 — Schritt-für-Schritt-Guide

Svelte 5 bietet einen Kompatibilitätsmodus: du kannst Svelte-4-Komponenten und Svelte-5-Komponenten im selben Projekt mischen. Eine schrittweise Migration ist also möglich. Das Svelte-Team empfiehlt, mit neuen Komponenten zu starten und alte iterativ zu migrieren.

Automatische Migration

npx sv migrate svelte-5 migriert einfache Komponenten vollautomatisch. Komplexe Stores, use:-Actions und benutzerdefinierte Store-Logik erfordern manuelle Anpassung.

1. Stores → $state (in .svelte.ts)

// Svelte 4 — stores/counter.ts
import { writable, derived } from 'svelte/store';

export const count   = writable(0);
export const doubled = derived(count, $c => $c * 2);

// Svelte 5 — counter.svelte.ts (Dateiendung beachten!)
export function createCounter() {
  let count   = $state(0);
  let doubled = $derived(count * 2);

  return {
    get count()   { return count;   },
    get doubled() { return doubled; },
    increment() { count++; },
    reset()     { count = 0; },
  };
}

// In Komponenten:
const counter = createCounter();
// counter.count, counter.doubled sind reaktiv ✓

2. Reactive Declarations → $derived

// Svelte 4
let firstName = 'Max';
let lastName  = 'Mustermann';
$: fullName = `${firstName} ${lastName}`;
$: initials = `${firstName[0]}.${lastName[0]}.`;
$: {
  // reaktiver Block
  console.log('Name geändert:', fullName);
}

// Svelte 5
let firstName = $state('Max');
let lastName  = $state('Mustermann');
let fullName  = $derived(`${firstName} ${lastName}`);
let initials  = $derived(`${firstName[0]}.${lastName[0]}.`);
$effect(() => {
  console.log('Name geändert:', fullName);
});

3. Lifecycle → $effect

// Svelte 4
import { onMount, onDestroy, afterUpdate } from 'svelte';

onMount(() => {
  analytics.track('page_view');
});
let interval;
onMount(() => {
  interval = setInterval(tick, 1000);
});
onDestroy(() => clearInterval(interval));

// Svelte 5 — alles in einem $effect
$effect(() => {
  analytics.track('page_view');

  const interval = setInterval(tick, 1000);
  return () => clearInterval(interval); // onDestroy
});

4. Slots → Snippets

<!-- Svelte 4 -->
<div class="card">
  <slot name="header" />
  <slot />
  <slot name="footer" />
</div>

<!-- Svelte 5 -->
<script>
  let { header, children, footer } = $props();
</script>

<div class="card">
  {#if header}{@render header()}{/if}
  {@render children()}
  {#if footer}{@render footer()}{/if}
</div>
Migration Checkliste
  • let x = 0let x = $state(0) für reaktiven Zustand
  • $: derived = exprlet derived = $derived(expr)
  • $: { block }$effect(() => { block })
  • export let proplet { prop } = $props()
  • export let bindablelet { bindable = $bindable() } = $props()
  • <slot />{@render children()}
  • <slot name="x" />{@render x()} (x als Prop)
  • onMount/onDestroy$effect mit Return-Cleanup
  • writable() Store → $state() in .svelte.ts
  • derived() Store → $derived() in .svelte.ts

KI-gestützte Svelte-5-Entwicklung mit Claude Code

Lass Claude Code deine Svelte-4-Komponenten automatisch in Runes migrieren, Snippets generieren und TypeScript-Props-Interfaces ableiten — direkt im Terminal, ohne Kontextwechsel.

Jetzt kostenlos testen →

7 Fazit — Warum Runes ein Schritt nach vorne sind

Svelte 5 Runes fühlen sich zunächst wie zusätzliche Syntax an — aber das Gegenteil ist wahr: Das System ist kleiner und konsistenter als das Svelte-4-Reaktivitätsmodell. Keine geheimen Compiler-Tricks mehr, keine $:-Regeln die außerhalb von .svelte-Dateien nicht funktionieren, keine Store-Subscribe/Unsubscribe-Fehler mehr.

Die sechs Runes ($state, $derived, $effect, $props, $bindable, $inspect) ersetzen ein halbes Dutzend unterschiedlicher Konzepte aus Svelte 4 — und lassen sich dank der .svelte.ts-Dateien nun auch außerhalb von Komponenten nutzen. Das ermöglicht echte Kompositions-Patterns à la React Hooks, aber ohne deren Fallen (Rules of Hooks, Closure-Stale-State etc.).

Mit Claude Code als KI-Pair-Programmer lassen sich Migrationen beschleunigen: Claude erkennt automatisch $:-Deklarationen, schlägt das passende Rune-Äquivalent vor und generiert TypeScript-Types für Props-Interfaces. Ein Svelte-4-Projekt von 50 Komponenten kann so in wenigen Stunden auf Runes umgestellt werden.