# Svelte Stores: Zustand außerhalb der Komponente

URL: https://www.mikebild.dev/de/blog/svelte-stores-zustand-ausserhalb-der-komponente/

Wer eine State-Management-Bibliothek installiert, bevor die Besitzfrage geklärt ist, hat den Preis schon bezahlt: eine zusätzliche Abhängigkeit, ein zweites mentales Modell und Boilerplate – für ein Problem, das das Projekt womöglich gar nicht hat. Der Auslöser ist meist harmlos. Zwei Komponenten, die nicht direkt verwandt sind, sollen sich einen Wert teilen; die Reaktivität innerhalb einer Komponente funktioniert wunderbar, aber über die Komponentengrenze hinweg fehlt der Mechanismus. Aus der React-Welt kennt man den Griff zur großen Bibliothek, und er passiert reflexartig, obwohl die eigentliche Frage keine API-Frage ist: Wem gehört dieser Zustand, und wie viel Maschinerie darf er kosten?

Svelte hat für diesen Fall bereits ein Werkzeug an Bord, das kaum eine Handvoll Funktionen umfasst – Stores. Sie beantworten die Besitzfrage mit dem kleinstmöglichen Mittel und lassen die Kosten erst dann steigen, wenn ein konkretes Problem es erzwingt. Der eigentliche Gewinn liegt dabei weniger in den Funktionen selbst als in der Disziplin, die sie erlauben: klein anfangen und die Grenze zu „mehr“ bewusst ziehen statt sie reflexartig zu überschreiten.

Ein Store ist Zustand, der außerhalb der Komponente steht. Er gehört keiner einzelnen `.svelte`-Datei, sondern liegt in einem separaten Modul und kann von überall aus abonniert werden. Damit löst er ein Problem, das in Komponentenbäumen schnell lästig wird: das Durchreichen von Werten durch mehrere Ebenen, nur damit ein tief verschachteltes Kind an eine Information kommt. Wer meinen Artikel zur [Svelte-Reaktivität](https://www.mikebild.dev/de/blog/svelte-reaktivitaet-compiler-ohne-virtual-dom/) gelesen hat, kennt bereits das Fundament: Der Compiler baut die Reaktivität zur Bauzeit ein, ohne Virtual DOM. Stores sind die logische Fortsetzung dieses Gedankens für Zustand, der die Komponentengrenze überschreiten muss.

**Redaktionelle Einordnung (Juli 2026):** Der Text verwendet das Store-Modell von Svelte 3/4. Stores sind in Svelte 5 weiterhin unterstützt und besonders für asynchrone Streams oder manuell kontrollierte Subscriptions nützlich. Für geteilten Zustand kann heute aber auch eine `.svelte.js`- oder `.svelte.ts`-Datei mit Runes genügen; Stores sind nicht mehr die einzige eingebaute Antwort außerhalb einer Komponente.

## Die Besitzfrage steht vor der Werkzeugfrage

Bevor ich zu den konkreten Svelte-Funktionen komme, ein Prinzip, das mit Svelte selbst nichts zu tun hat und in jedem komponentenbasierten Frontend gilt – ob React, Vue oder Svelte. Ich habe es an anderer Stelle für React ausführlich hergeleitet, im [State-Management-Vergleich](https://www.mikebild.dev/de/blog/state-management-vergleich/), und der Kern trägt eins zu eins hierher: Die entscheidende Frage lautet nicht „welche State-Bibliothek“, sondern „wem gehört dieser Zustand?“. Ein Zustand gehört an die tiefste Stelle, an der ihn alle Beteiligten noch erreichen. Solange das eine einzelne Komponente ist, bleibt er dort – lokal, ohne jede Zeremonie. Erst wenn sich mehrere, nicht verwandte Komponenten denselben Wert teilen oder er von außerhalb des Baums kommt, verlässt er die Komponente. Ein globaler oder modulweiter Store ist damit die Ausnahme, die man begründet, nicht der Normalfall, in den man hineinfällt.

Warum ich das so betone, ist ein Kostenargument. Jede Ebene, auf die ich Zustand hebe, macht ihn teurer: mehr Stellen, die schreiben dürfen, schwerer nachvollziehbare Änderungen, eine größere Fläche für Fehler. In Projekten habe ich den umgekehrten Weg oft genug mitgemacht – erst wird die große Bibliothek gewählt, dann wird Zustand hineingeschoben, bis alles global ist. Die Gegenprobe geht andersherum: Braucht dieser Wert wirklich mehrere Komponenten, und sind sie so entkoppelt, dass eine gemeinsame Elternkomponente sie nicht ohnehin verbinden würde? In meiner Erfahrung bleibt nach dieser Frage deutlich weniger globaler Zustand übrig als anfangs vermutet.

Für die Entscheidung, ob ein Wert die Komponente verlassen soll, nutze ich drei Kriterien. Die Reichweite: Teilen ihn mindestens zwei Komponenten, die nicht in einer sauberen Eltern-Kind-Beziehung stehen? Die Herkunft: Wird der Wert von außerhalb des Baums geschrieben – Timer, WebSocket, Sensor? Und die Lebensdauer: Muss er unabhängig davon existieren, ob eine bestimmte Komponente gerade gemountet ist? Trifft keines zu, bleibt der Zustand lokal, und jede darüberliegende Lösung ist bezahlte Komplexität ohne Gegenwert. Trifft eines zu, ist ein schlanker, modulweiter Store die richtige Antwort – in Svelte buchstäblich eine Zeile Code. Die eigentliche Arbeit steckt in der Entscheidung davor, nicht in der API danach.

## Der Contract, nicht die Klasse

Ein Store ist in Svelte kein spezieller Typ und keine Klasse, von der man erben muss. Ein Store ist jedes Objekt, das eine `.subscribe`-Methode nach einem festen Vertrag bereitstellt – und optional eine `.set`-Methode, wenn es schreibbar sein soll. `subscribe` muss den aktuellen Wert synchron an den Subscriber liefern und eine Funktion zum Abbestellen zurückgeben. Genau dieser Contract ist es, der zählt. Alles, was ihn erfüllt, funktioniert mit der `$`-Syntax.

In der Praxis schreibt man diese `.subscribe`-Methode fast nie selbst. Svelte liefert drei Fabrikfunktionen mit, die einem die Arbeit abnehmen. Sie stammen alle aus demselben Modul:

```js
import { writable, readable, derived, get } from 'svelte/store';
```

Die drei Store-Typen decken drei unterschiedliche Rollen ab:

- `writable` – les- und schreibbar. Der Standardfall für geteilten Zustand, den irgendeine Komponente oder Funktion verändern darf. Bietet `.set(value)`, `.update(fn)` und `.subscribe(run)`.
- `readable` – nur lesbar von außen. Der Wert wird intern über einen Start-Callback gesetzt, ideal für externe Quellen wie eine Uhr, einen WebSocket oder einen Event-Listener.
- `derived` – abgeleitet. Berechnet seinen Wert reaktiv aus einem oder mehreren anderen Stores und aktualisiert sich automatisch, wenn sich eine seiner Quellen ändert.

## Der writable Store und die $-Kurzschreibweise

Fangen wir mit dem häufigsten Fall an – einem gemeinsamen Zähler, den mehrere Komponenten teilen. Der Store liegt in einem eigenen Modul, damit ihn jede Komponente importieren kann:

```js
// stores.js
import { writable } from 'svelte/store';

export const count = writable(0);
```

Das ist der komplette Store. Ein Modul, eine Zeile, ein Anfangswert. Jetzt kann eine beliebige Komponente diesen Zähler lesen und schreiben – und hier kommt die eleganteste Idee von Svelte ins Spiel, die `$`-Kurzschreibweise:

```svelte
<script>
  import { count } from './stores.js';
</script>

<button on:click={() => $count++}>
  Clicked {$count} times
</button>
```

Das `$count` im Template ist keine magische globale Variable, sondern eine Anweisung an den Compiler. Er erkennt das Präfix `$` vor einer Store-Referenz und generiert daraus zweierlei: erstens ein `count.subscribe(...)`, das den aktuellen Wert in eine lokale Variable schreibt und das Template bei jeder Änderung neu rendert; zweitens – und das ist der entscheidende Teil – ein automatisches `unsubscribe`, sobald die Komponente zerstört wird. Man muss sich um das Aufräumen also nicht kümmern, der Compiler nimmt es einem ab.

Auch das Schreiben ist über die Kurzschreibweise abgedeckt. `$count++` übersetzt der Compiler in einen `count.set(...)`-Aufruf mit dem inkrementierten Wert. Wer es expliziter mag oder eine komplexere Änderung braucht, greift zu den Methoden direkt:

```svelte
<script>
  import { count } from './stores.js';

  function increment() {
    count.update(n => n + 1);
  }

  function reset() {
    count.set(0);
  }
</script>

<button on:click={increment}>+1</button>
<button on:click={reset}>Reset</button>
<p>Current value: {$count}</p>
```

Der Unterschied zwischen `set` und `update` ist schnell erklärt. `set` überschreibt den Wert bedingungslos, `update` bekommt den aktuellen Wert als Argument und gibt den neuen zurück – praktisch immer dann, wenn der neue Wert vom alten abhängt.

Und jetzt der Kern des Ganzen: Sobald eine zweite Komponente denselben Store importiert und `$count` verwendet, teilen sich beide denselben Zustand. Ändert die eine den Wert, sieht ihn die andere sofort. Kein Prop wird durch den Baum gereicht, keine gemeinsame Elternkomponente muss den Zustand halten. Der Store ist die gemeinsame Quelle, an der alle Beteiligten hängen.

```mermaid
graph TD
  S["writable count<br/>(stores.js)"]
  A["Component A<br/>$count lesen + schreiben"]
  B["Component B<br/>$count anzeigen"]
  C["Component C<br/>$count anzeigen"]
  S --> A
  S --> B
  S --> C
  A -. "count.set()" .-> S
```

Man sieht hier gut, was der Store leistet: Er entkoppelt die Komponenten voneinander. A, B und C müssen weder verwandt sein noch voneinander wissen. Sie kennen nur den Store.

## Abgeleiteter Zustand mit derived

Oft braucht man einen Wert, der sich aus einem anderen ergibt. Man könnte ihn in jeder Komponente neu ausrechnen – oder man leitet ihn einmal zentral ab und alle profitieren. Genau dafür ist `derived` da:

```js
// stores.js
import { writable, derived } from 'svelte/store';

export const count = writable(0);

export const doubled = derived(count, $count => $count * 2);
```

`doubled` ist selbst wieder ein Store, aber ein nur lesbarer. Er hat keine `.set`-Methode, denn sein Wert ergibt sich vollständig aus `count`. Ändert sich `count`, berechnet `derived` den Wert neu und benachrichtigt seine Abonnenten. In der Komponente sieht das dann so aus, als wäre es ein ganz normaler Store:

```svelte
<script>
  import { count, doubled } from './stores.js';
</script>

<p>{$count} doubled is {$doubled}</p>
<button on:click={() => $count++}>+1</button>
```

`derived` kann auch aus mehreren Quellen ableiten. Dann übergibt man ein Array von Stores und bekommt im Callback ein Array der Werte:

```js
import { writable, derived } from 'svelte/store';

export const price = writable(100);
export const quantity = writable(2);

export const total = derived(
  [price, quantity],
  ([$price, $quantity]) => $price * $quantity
);
```

Neben dieser synchronen Form gibt es eine zweite, die einen `set`-Callback bekommt. Damit lässt sich der abgeleitete Wert verzögert oder bedingt setzen – etwa nach einem Timeout oder erst, wenn eine bestimmte Bedingung erfüllt ist:

```js
import { derived } from 'svelte/store';

// Sets the value one second after each change of 'count'
export const delayed = derived(
  count,
  ($count, set) => {
    const id = setTimeout(() => set($count), 1000);
    return () => clearTimeout(id);
  }
);
```

Der Rückgabewert der Callback-Funktion ist hier eine Aufräumfunktion, die Svelte vor der nächsten Berechnung und beim letzten Unsubscribe ausführt. So bleibt kein Timeout hängen.

## readable für externe Quellen

Nicht jeder Zustand wird von der Anwendung selbst geschrieben. Manchmal kommt er von außen – eine Uhr, die tickt, ein WebSocket, der Nachrichten liefert, ein Sensor. Für solche Fälle gibt es `readable`. Von außen kann man ihn nur lesen; wer ihn füttert, ist ein Start-Callback, der die Quelle aufsetzt:

```js
import { readable } from 'svelte/store';

export const time = readable(null, set => {
  const id = setInterval(() => set(new Date()), 1000);

  return () => clearInterval(id);
});
```

Hier lohnt sich ein genauer Blick auf das Timing, weil es ein zentrales Prinzip von Stores offenlegt. Der Start-Callback läuft nicht sofort beim Anlegen des Stores. Er läuft erst, wenn der erste Abonnent hinzukommt – wenn die Zahl der Subscriber von null auf eins springt. Und die zurückgegebene Funktion – der Stop-Teil – läuft, wenn der letzte Abonnent abspringt, also von eins auf null. Solange niemand zuhört, tickt bei diesem `readable` kein Intervall und öffnet sich kein Socket. Bei `writable` bleiben `set` und `update` auch ohne Subscriber wirksam; lazy ist nur eine optionale Start-/Stop-Arbeit. `derived` wiederum berechnet für seine Abonnenten und meldet sich ohne sie von den Quellen ab.

In der Komponente ist der Umgang wieder trivial, denn `$time` abonniert automatisch und räumt beim Unmount auf:

```svelte
<script>
  import { time } from './stores.js';
</script>

<p>The time is {$time?.toLocaleTimeString()}</p>
```

## Der Fallstrick: subscribe ohne unsubscribe

Bisher lief alles über die `$`-Kurzschreibweise, und dort ist das Aufräumen kein Thema. Aber Stores sind nicht auf Komponenten beschränkt. Man kann sie genauso in einem reinen JavaScript-Modul verwenden – in einer Utility-Funktion, einem Service, irgendwo außerhalb von Svelte. Und genau da lauert der häufigste Fehler.

Außerhalb einer Komponente gibt es kein `$` und keinen Compiler, der aufräumt. Man ruft `subscribe` von Hand auf, und die Methode gibt eine Unsubscribe-Funktion zurück. Diese Funktion **muss** man aufheben und aufrufen, sonst hat man ein Leak:

```js
// utils.js
import { count } from './stores.js';

// WRONG: the callback runs forever.
count.subscribe(value => {
  console.log('count is now', value);
});
```

Der Rückgabewert von `subscribe` wird hier einfach ignoriert. Solange das Modul beziehungsweise dieser Nutzungskontext lebt, bleibt der Callback registriert. Der Store zählt weiterhin einen Abonnenten, der Stop-Callback von 1→0 wird nicht ausgelöst – bei einem `readable` mit Intervall liefe der Timer entsprechend weiter. Richtig ist es so:

```js
// utils.js
import { count } from './stores.js';

const unsubscribe = count.subscribe(value => {
  console.log('count is now', value);
});

// ... later, when no longer needed:
unsubscribe();
```

Dieselbe Regel gilt sinngemäß auch innerhalb von Komponenten, falls man dort aus irgendeinem Grund manuell `subscribe` statt `$` verwendet: Dann muss man den Unsubscriber in `onDestroy` aufrufen. Aber ehrlich gesagt ist das fast immer ein Zeichen, dass man einfach die `$`-Syntax hätte nehmen sollen. Genau diese Buchhaltung nimmt sie einem ab.

Wenn man den Wert nur ein einziges Mal lesen will – ohne Abonnement, ohne Reaktivität – gibt es einen sauberen Weg, der das Leak-Problem gar nicht erst aufkommen lässt:

```js
import { get } from 'svelte/store';
import { count } from './stores.js';

const current = get(count);
```

`get` abonniert intern kurz, liest den aktuellen Wert und meldet sich sofort wieder ab. Der Wert ist eine Momentaufnahme und aktualisiert sich nicht mehr. Genau deshalb passt `get` gut für einmaliges Auslesen in einem JS-Modul – und gerade nicht in einen heißen Pfad, der ständig durchlaufen wird.

```mermaid
sequenceDiagram
  participant C as Erster Subscriber
  participant S as Store
  participant Q as Externe Quelle
  C->>S: subscribe(run)
  S->>Q: start-Callback (0 -> 1)
  Q-->>S: Werte fließen
  S-->>C: run(value)
  C->>S: unsubscribe()
  S->>Q: stop-Callback (1 -> 0)
```

## Wann Store, und wann nicht

Der Store ist das richtige Werkzeug, sobald Zustand nicht mehr sauber in eine einzelne Komponente passt – wenn mehrere, nicht direkt verwandte Komponenten sich denselben Wert teilen, oder wenn ein Wert von außerhalb des Komponentenbaums kommt. Das klassische Symptom, das nach einem Store ruft, ist Prop-Drilling: Man reicht einen Wert durch drei, vier Ebenen, obwohl ihn nur die unterste Komponente wirklich braucht. Ein Store schneidet diesen Durchreiche-Pfad ab. Die betroffenen Komponenten importieren den Store direkt, und die Zwischenebenen müssen von dem Wert gar nichts wissen.

Bleibt die Frage, ob es daneben noch eine große State-Management-Bibliothek braucht. Für die allermeisten Svelte-Anwendungen: nein. Die drei Store-Typen decken erstaunlich viel ab. `writable` für den Zustand, den man verändert, `derived` für alles, was sich daraus ergibt, `readable` für externe Quellen. Das ist kein Kompromiss, kein „reicht erstmal“ – es ist für den Großteil realer Anwendungen schlicht ausreichend. Ein zentrales Store-Modul, das die einzelnen Stores exportiert, gibt der Anwendung eine klare, überschaubare Struktur, ohne dass man ein weiteres mentales Modell und eine weitere Abhängigkeit mitschleppen muss.

Wo liegt dann der Rand, ab dem man über mehr nachdenkt? Ich würde sagen: Erst wenn der Zustand wirklich komplex wird – viele voneinander abhängige Übergänge, strikte Nachvollziehbarkeit jeder Änderung, ausgefeiltes Time-Travel-Debugging über eine große Domäne hinweg. Solche Anforderungen sind selten, und selbst dann baut man häufig eine dünne Schicht auf Stores auf, statt sie zu ersetzen. Solange die Frage aber nur lautet „Wie teilen sich meine Komponenten einen Wert?“, ist die Antwort ein Store – und mehr braucht es nicht.

Was nach mehreren Projekten mit Svelte bleibt, ist weniger eine API-Empfehlung als eine Reihenfolge: erst den Besitzer des Zustands bestimmen, dann das Werkzeug. Für das 2022 gezeigte Modell sind `writable`, `readable` und `derived` oft genug. Die `$`-Kurzschreibweise erledigt in Komponenten die Subscription-Buchhaltung; außerhalb bleibt die schlichte Regel, jedes manuelle `subscribe` mit einem `unsubscribe` zu paaren.

## Weiterführende Quellen

- Foliensatz zu diesem Thema: https://supabase.mikebild.dev/storage/v1/object/public/slides/introduction-svelte.html
- Repository: [mikebild.dev](https://www.mikebild.dev) – dieser und weitere Artikel zu Svelte und Frontend-Architektur
- [Svelte-Reaktivität: der Compiler ohne Virtual DOM](https://www.mikebild.dev/de/blog/svelte-reaktivitaet-compiler-ohne-virtual-dom/)
- [Svelte-Docs – Stores und Runes](https://svelte.dev/docs/svelte/stores)
