Zustandslogik sauber modellieren
State Machines modellieren erlaubte Zustände und Übergänge explizit, statt sie implizit über verstreute Boolean-Flags zu erraten. XState bringt dieses Konzept als framework-unabhängige Bibliothek nach JavaScript, mit States, Events, Guards und hierarchischen Statecharts, die in React, Vue oder purem JavaScript gleichermaßen funktionieren.
Inhaltsverzeichnis
- 1. Warum State Machines UI-Logik besser modellieren
- 2. Grundkonzepte: States, Events, Transitions
- 3. XState Grundlagen: createMachine und Statecharts
- 4. Guards und Actions: Bedingungen und Seiteneffekte
- 5. Hierarchische und parallele States
- 6. Context: erweiterter Zustand neben endlichen States
- 7. Integration in React, Vue und Vanilla JavaScript
- 8. Testing von State Machines
- 9. State Machines im Vergleich
- 10. Zusammenfassung
- 11. FAQ
1. Warum State Machines UI-Logik besser modellieren
Eine State Machine definiert eine endliche Menge erlaubter Zustände sowie die Events, die Übergänge zwischen ihnen auslösen dürfen. Der Gegenentwurf, den man in vielen Codebasen findet, sind mehrere unabhängige Boolean-Flags wie isLoading, hasError und isSuccess, die theoretisch 2 hoch 3 Kombinationen zulassen, obwohl nur vier davon tatsächlich sinnvoll sind. Eine State Machine macht diese impliziten Regeln explizit, indem sie nur die tatsächlich gültigen Zustände wie idle, loading, success und error überhaupt zulässt.
Dieser Unterschied wird besonders bei komplexen Formularen, Checkout-Flows oder Datei-Uploads sichtbar, wo unmögliche Zustände wie gleichzeitig isLoading und hasError in der Praxis zu widersprüchlichen UI-Zuständen führen, etwa einem Spinner neben einer Fehlermeldung. Eine State Machine verhindert solche Widersprüche strukturell, weil ein Zustand immer eindeutig ist und ein Übergang immer explizit definiert sein muss, sonst wird das Event schlicht ignoriert.
XState ist die verbreitetste JavaScript-Bibliothek für State Machines und implementiert dabei das SCXML-inspirierte Statechart-Modell nach David Harel, erweitert um hierarchische und parallele Zustände. Die folgenden Abschnitte zeigen den Aufbau einer State Machine von den Grundkonzepten über Guards und Context bis zur Integration in bestehende Frontend-Anwendungen.
2. Grundkonzepte: States, Events, Transitions
Die drei Grundbegriffe jeder State Machine sind States, Events und Transitions. Ein State beschreibt eine benannte Konfiguration des Systems zu einem bestimmten Zeitpunkt, etwa idle oder submitting. Ein Event ist ein benanntes Signal, das von außen an die Maschine geschickt wird, etwa SUBMIT oder RETRY. Eine Transition verbindet einen State mit einem Event und definiert den Zielzustand, in den die Maschine wechselt, wenn dieses Event in diesem State eintrifft.
Entscheidend ist, dass eine State Machine nur Events verarbeitet, die für den aktuellen State explizit definiert sind. Trifft ein SUBMIT-Event ein, während die Maschine bereits im State submitting ist, und keine Transition dafür definiert wurde, bleibt die Maschine unverändert im submitting-State, das Event wird verworfen. Dieses Verhalten unterscheidet State Machines fundamental von Event-Emittern, die jedes Event unabhängig vom aktuellen Zustand verarbeiten.
3. XState Grundlagen: createMachine und Statecharts
XState definiert eine State Machine deklarativ über createMachine() mit einem Konfigurationsobjekt, das States, den Startzustand und Transitions als verschachtelte Struktur beschreibt. Diese Deklarativität ist ein zentraler Vorteil: die gesamte Zustandslogik lässt sich als Diagramm visualisieren, etwa im XState-Visualizer, und ist unabhängig vom Rendering-Code lesbar. Ein Entwickler kann die Statechart-Definition prüfen, ohne den restlichen Anwendungscode zu kennen.
Der interpret()-Aufruf, in XState 5 createActor(), erzeugt eine lauffähige Instanz der State Machine, einen sogenannten Actor, der Events per send() entgegennimmt und über subscribe() Zustandsänderungen nach außen meldet. Dieser Actor ist framework-unabhängig, er funktioniert identisch in einer Node.js-CLI, einer React-Komponente oder einem Vue-Composable.
// Basic state machine: fetch flow with idle, loading, success, error
import { createMachine, createActor, assign } from 'xstate';
const fetchMachine = createMachine({
id: 'fetch',
initial: 'idle',
context: { data: null, error: null },
states: {
idle: {
on: { FETCH: 'loading' },
},
loading: {
on: {
RESOLVE: { target: 'success', actions: assign({ data: ({ event }) => event.data }) },
REJECT: { target: 'error', actions: assign({ error: ({ event }) => event.error }) },
},
},
success: {
on: { FETCH: 'loading' },
},
error: {
on: { RETRY: 'loading' },
},
},
});
const actor = createActor(fetchMachine);
actor.subscribe((snapshot) => console.log(snapshot.value));
actor.start();
actor.send({ type: 'FETCH' }); // -> loading
4. Guards und Actions: Bedingungen und Seiteneffekte
Ein Guard ist eine reine Funktion, die vor einer Transition prüft, ob der Übergang tatsächlich stattfinden darf, basierend auf dem aktuellen Context oder Event-Daten. Erst wenn der Guard true zurückgibt, führt die State Machine die Transition aus. Ohne einen erfüllten Guard bleibt die Maschine im aktuellen State, selbst wenn das passende Event eintrifft, ein wichtiges Werkzeug für bedingte Übergänge wie eine Formularvalidierung vor dem Submit.
Actions dagegen sind Seiteneffekte, die bei einer Transition ausgeführt werden, etwa das Aktualisieren des Context über assign(), das Aufrufen einer externen API oder das Loggen eines Events. XState unterscheidet zwischen Entry-Actions, die beim Betreten eines States laufen, Exit-Actions beim Verlassen, und Transition-Actions, die genau während des Übergangs ausgeführt werden. Diese klare Trennung von Bedingung (Guard) und Effekt (Action) macht State Machines in XState besonders nachvollziehbar in Code-Reviews.
// Guards and actions: conditional transition with validation
const formMachine = createMachine({
id: 'form',
initial: 'editing',
context: { email: '' },
states: {
editing: {
on: {
SUBMIT: [
{
guard: ({ context }) => context.email.includes('@'),
target: 'submitting',
},
{ target: 'invalid' }, // fallback when guard fails
],
},
},
invalid: {
entry: () => console.warn('Validation failed'),
on: { EDIT: 'editing' },
},
submitting: {
entry: () => console.log('Submitting form...'),
on: { SUCCESS: 'done' },
},
done: { type: 'final' },
},
});
5. Hierarchische und parallele States
Hierarchische States, in XState auch Compound States genannt, erlauben es, einen State in verschachtelte Sub-States zu unterteilen, die gemeinsame Transitions vom Elternstate erben. Ein State authenticated kann etwa die Sub-States dashboard und settings enthalten, während eine übergeordnete Transition LOGOUT auf Elternebene definiert wird und aus jedem Sub-State heraus funktioniert, ohne sie einzeln zu duplizieren. Diese Struktur reduziert Redundanz erheblich gegenüber einer flachen State Machine mit vielen ähnlichen Transitions.
Parallele States gehen noch einen Schritt weiter: mehrere unabhängige Sub-Machines laufen gleichzeitig innerhalb desselben Elternstates, jede mit eigenem aktuellem Zustand. Ein typisches Beispiel ist ein Video-Player, bei dem der Wiedergabe-State (playing/paused) und der Lautstärke-State (muted/unmuted) unabhängig voneinander existieren, aber Teil derselben State Machine sind. Ohne parallele States müsste man das kartesische Produkt aller Kombinationen als einzelne States modellieren, was schnell unübersichtlich wird.
// Hierarchical (compound) and parallel states
const playerMachine = createMachine({
id: 'player',
type: 'parallel', // both regions run simultaneously
states: {
playback: {
initial: 'paused',
states: {
paused: { on: { PLAY: 'playing' } },
playing: { on: { PAUSE: 'paused' } },
},
},
volume: {
initial: 'unmuted',
states: {
unmuted: { on: { MUTE: 'muted' } },
muted: { on: { UNMUTE: 'unmuted' } },
},
},
},
});
// A shared LOGOUT transition on a parent state, inherited by every child
const sessionMachine = createMachine({
id: 'session',
initial: 'authenticated',
states: {
authenticated: {
on: { LOGOUT: 'anonymous' }, // works from dashboard AND settings
initial: 'dashboard',
states: {
dashboard: { on: { OPEN_SETTINGS: 'settings' } },
settings: { on: { CLOSE_SETTINGS: 'dashboard' } },
},
},
anonymous: { on: { LOGIN: 'authenticated' } },
},
});
6. Context: erweiterter Zustand neben endlichen States
Neben den endlichen States braucht fast jede reale State Machine zusätzliche Daten, die nicht als eigener State modelliert werden sollen, etwa den Inhalt eines Formularfelds oder die Anzahl fehlgeschlagener Versuche. Dafür bietet XState den Context, ein beliebiges Datenobjekt, das über assign()-Actions aktualisiert wird, während die endlichen States weiterhin die grobe Struktur des Ablaufs vorgeben.
Diese Trennung zwischen endlichen States für die Ablaufstruktur und Context für erweiterte Daten ist entscheidend für die Lesbarkeit: die Statechart-Visualisierung zeigt weiterhin nur eine überschaubare Anzahl von States, während Context beliebig komplexe Daten tragen kann, ohne die Anzahl der States explodieren zu lassen. Retry-Zähler, die nach drei Versuchen einen anderen State erzwingen, sind ein klassisches Muster, bei dem Context und Guards zusammenspielen.
// Context alongside finite states: retry counter driving a guard
const uploadMachine = createMachine({
id: 'upload',
initial: 'idle',
context: { retries: 0, maxRetries: 3 },
states: {
idle: { on: { UPLOAD: 'uploading' } },
uploading: {
on: {
SUCCESS: 'done',
FAILURE: [
{
guard: ({ context }) => context.retries < context.maxRetries,
target: 'uploading',
actions: assign({ retries: ({ context }) => context.retries + 1 }),
},
{ target: 'failed' }, // retries exhausted
],
},
},
done: { type: 'final' },
failed: { on: { RESET: 'idle' } },
},
});
7. Integration in React, Vue und Vanilla JavaScript
Der Hook useMachine() aus @xstate/react bindet eine State Machine an eine React-Komponente, liefert den aktuellen Snapshot und eine send-Funktion, und synchronisiert Re-Renders automatisch mit Zustandsänderungen. In Vue übernimmt @xstate/vue mit useMachine() dieselbe Aufgabe, angebunden an das Vue-Reactivity-System über ref. Beide Adapter sind dünne Wrapper um denselben framework-unabhängigen Actor-Kern.
In reinem Vanilla JavaScript oder Web Components lässt sich derselbe Actor direkt über createActor(machine).subscribe() nutzen, ohne zusätzlichen Adapter. Diese Konsistenz ist der praktische Vorteil von XState gegenüber Framework-eigenen State-Lösungen: dieselbe State Machine-Definition kann in einer React-App, einer Vue-App und einem reinen Vanilla-Skript unverändert wiederverwendet werden, weil die Zustandslogik komplett vom Rendering entkoppelt ist.
// React integration: useMachine binds the same actor core to a component
import { useMachine } from '@xstate/react';
function UploadButton() {
const [snapshot, send] = useMachine(uploadMachine);
return (
<button onClick={() => send({ type: 'UPLOAD' })}>
{snapshot.matches('uploading') ? 'Uploading...' : 'Upload'}
</button>
);
}
// Vanilla JavaScript, no framework adapter needed
const actor = createActor(uploadMachine);
actor.subscribe((snapshot) => {
document.querySelector('#status').textContent = snapshot.value;
});
actor.start();
8. Testing von State Machines
State Machines lassen sich isoliert vom Rendering testen, indem man einen Actor erzeugt, Events sendet und den resultierenden State prüft, ganz ohne DOM oder Testing-Library-Setup. Ein Test für die Übergänge FETCH zu loading, dann RESOLVE zu success läuft in Millisekunden und deckt die komplette Übergangslogik ab, unabhängig davon, wie die UI später gerendert wird.
XState bietet zusätzlich Model-Based-Testing über @xstate/test, das aus der Statechart-Definition automatisch Testpfade generiert, die jeden möglichen Zustand und jede mögliche Transition mindestens einmal durchlaufen. Dieser Ansatz deckt Randfälle auf, die in manuell geschriebenen Tests häufig übersehen werden, weil die Pfadgenerierung systematisch statt intuitiv erfolgt, ein deutlicher Vorteil gegenüber Ad-hoc-Tests einer Boolean-Flag-basierten Logik.
9. State Machines im Vergleich
Die Entscheidung für eine explizite State Machine gegenüber verstreuten Boolean-Flags oder einem generischen Redux-Reducer hängt von der Komplexität des Ablaufs ab: einfache An/Aus-Zustände profitieren kaum, komplexe Flows mit vielen möglichen Übergängen erheblich.
| Ansatz | Unmögliche Zustände | Visualisierbarkeit | Bester Einsatz |
|---|---|---|---|
| Boolean-Flags | Möglich, häufiges Problem | Keine | Sehr einfache, isolierte Zustände |
| Redux-Reducer | Möglich, ohne Disziplin | Gering | Generischer globaler Zustand |
| XState State Machine | Strukturell ausgeschlossen | Statechart-Diagramm | Komplexe Flows mit klaren Übergängen |
| Enum + Switch | Teilweise ausgeschlossen | Gering | Mittlere Komplexität ohne Bibliothek |
In der Praxis zeigt sich der Wert einer State Machine besonders bei Checkout-Prozessen, mehrstufigen Formularen und Datei-Uploads, wo die Anzahl möglicher Zustandskombinationen mit jedem zusätzlichen Boolean-Flag exponentiell wächst. XState macht diese Komplexität explizit und testbar, statt sie implizit im Code verteilt zu lassen.
Mironsoft
Zustandslogik, Checkout-Flows und komplexe UI-Abläufe
Checkout-Logik ohne widersprüchliche Zustände?
Wir modellieren komplexe UI-Abläufe als XState State Machines, mit klaren Guards, testbaren Transitions und Statechart-Diagrammen, die das gesamte Team versteht.
Flow-Audit
Analyse bestehender Boolean-Flag-Logik auf unmögliche Zustandskombinationen
XState-Migration
Umbau kritischer Flows in getestete, visualisierbare State Machines
Team-Schulung
Statechart-Konzepte praxisnah vermittelt, direkt am eigenen Codebestand
10. Zusammenfassung
State Machines ersetzen implizite, verstreute Boolean-Flags durch explizit definierte States, Events und Transitions, die unmögliche Zustandskombinationen strukturell ausschließen. XState implementiert dieses Modell als framework-unabhängige Bibliothek mit hierarchischen und parallelen States, Guards für Bedingungen, Actions für Seiteneffekte und Context für erweiterte Daten neben den endlichen States selbst.
Der praktische Nutzen zeigt sich am stärksten bei komplexen Flows wie Checkout-Prozessen, mehrstufigen Formularen oder Datei-Uploads, wo die Anzahl möglicher Kombinationen mit jedem zusätzlichen Flag exponentiell wächst. Eine State Machine macht diese Komplexität sichtbar, testbar und in einem Diagramm nachvollziehbar, statt sie über Dutzende verstreuter if-Bedingungen zu verstecken.
State Machines mit XState — Das Wichtigste auf einen Blick
Grundmodell
States, Events und Transitions ersetzen verstreute Boolean-Flags und schließen unmögliche Zustände aus.
Guards & Actions
Guards prüfen Bedingungen vor Übergängen, Actions führen Seiteneffekte während der Transition aus.
Hierarchie & Context
Compound und parallele States strukturieren komplexe Flows, Context trägt erweiterte Daten.
Framework-Integration
@xstate/react und @xstate/vue binden denselben Actor-Kern an unterschiedliche Frameworks.