Inhalt

  1. XState v5 Grundlagen & Setup
  2. Guards & Conditions
  3. Actions & Effects
  4. Delays & After-Transitions
  5. Actor Model & Parallel States
  6. React Integration & Visualizer

Komplexe UI-Zustände sind der Alptraum jedes Frontend-Entwicklers. Lade-Spinner, Fehlerzustände, Retry-Logik, modale Dialoge mit Abhängigkeiten — wenn man das alles mit useState und useEffect abbildet, entsteht schnell ein unwartbares Geflecht aus booleans und Seiteneffekten. XState löst dieses Problem durch formale State Machines und das Actor Model.

Mit Claude Code lässt sich XState v5 besonders effektiv einsetzen: Der KI-Assistent versteht das State-Machine-Konzept auf Modellebene, generiert typensichere TypeScript-Definitionen, schlägt sinnvolle Guards und Actions vor und erklärt im Dialog, warum bestimmte Transitionen modelliert werden müssen. Dieses Tutorial zeigt den kompletten Workflow — von der Installation bis zur React-Integration mit Visualizer.

💡 Voraussetzungen: React 18+, TypeScript 5+, Node 20+. Grundkenntnisse in React-Hooks werden vorausgesetzt. XState-Erfahrung ist nicht nötig — Claude Code erklärt die Konzepte on-the-fly.

1. XState v5 Grundlagen & Setup

XState v5 bringt eine grundlegend überarbeitete API mit sich. Statt der alten Machine()-Funktion nutzt man jetzt createMachine(), und das Actor Model steht im Mittelpunkt. Claude Code kennt beide API-Versionen — frag explizit nach v5, um die aktuelle Syntax zu erhalten.

Installation

# XState v5 + React-Integration installieren npm install xstate@5 @xstate/react@4 # Optional: Stately Visualizer (Dev-Only) npm install --save-dev @statelyai/inspect
createMachine

Erstes Beispiel: Fetch-State-Machine

Claude Code Prompt: "Erstelle eine XState v5 State Machine für einen API-Fetch mit idle, loading, success und error States. TypeScript, vollständig typisiert."

import { createMachine, assign } from 'xstate'; // TypeScript-Typen für Context und Events interface FetchContext { data: string[] | null; error: string | null; retries: number; } type FetchEvent = | { type: 'FETCH'; url: string } | { type: 'SUCCESS'; data: string[] } | { type: 'ERROR'; message: string } | { type: 'RETRY' } | { type: 'RESET' }; export const fetchMachine = createMachine( { /** @xstate-layout N4Igp... */ id: 'fetch', initial: 'idle', context: { data: null, error: null, retries: 0, } satisfies FetchContext, states: { idle: { on: { FETCH: { target: 'loading', actions: 'clearError', }, }, }, loading: { on: { SUCCESS: { target: 'success', actions: 'setData', }, ERROR: { target: 'error', actions: 'setError', }, }, }, success: { on: { RESET: { target: 'idle', actions: 'clearAll' }, FETCH: { target: 'loading', actions: 'clearError' }, }, }, error: { on: { RETRY: { target: 'loading', guard: 'canRetry', actions: 'incrementRetries', }, RESET: { target: 'idle', actions: 'clearAll' }, }, }, }, }, { actions: { setData: assign({ data: (_, event) => (FetchEvent & { type: 'SUCCESS' } ? event.data : null) }), setError: assign({ error: (_, event) => (FetchEvent & { type: 'ERROR' } ? event.message : null) }), clearError: assign({ error: null }), clearAll: assign({ data: null, error: null, retries: 0 }), incrementRetries: assign({ retries: (ctx) => ctx.retries + 1 }), }, guards: { canRetry: (ctx) => ctx.retries < 3, }, } );

Die Machine ist vollständig deklarativ: Jeder Zustand beschreibt nur, welche Events er akzeptiert und wohin er transitiert. Claude Code generiert nicht nur den Code — auf Nachfrage erklärt es auch, warum der error-State den RETRY-Event nur mit dem canRetry-Guard akzeptiert, und nicht einfach direkt zurück zu loading wechselt.

⚠️ XState v5 Breaking Change: assign akzeptiert in v5 Objekte mit Updater-Funktionen statt des alten zweiten Parameters. Claude Code kennt den Unterschied — frag explizit: "XState v5 assign syntax".

2. Guards & Conditions

Guards sind Bedingungsfunktionen, die bestimmen, ob eine Transition stattfindet. In XState v5 können Guards als Strings referenziert, inline definiert oder parametrisiert werden. Claude Code hilft besonders bei der Komposition mehrerer Guards auf einer Transition.

Guards

Guard-Typen im Überblick

import { createMachine, and, or, not } from 'xstate'; interface CheckoutContext { cartTotal: number; isAuthenticated: boolean; hasValidPayment: boolean; discountCode: string | null; stockAvailable: boolean; userAge: number; } type CheckoutEvent = | { type: 'SUBMIT' } | { type: 'APPLY_DISCOUNT'; code: string } | { type: 'CONFIRM_AGE' }; const checkoutMachine = createMachine( { id: 'checkout', initial: 'review', context: { cartTotal: 0, isAuthenticated: false, hasValidPayment: false, discountCode: null, stockAvailable: true, userAge: 0, } satisfies CheckoutContext, states: { review: { on: { // Inline Guard — direkt als Funktion APPLY_DISCOUNT: { guard: (ctx, event) => { const validCodes = ['SAVE10', 'SPRING26', 'WELCOME']; return validCodes.includes(event.code); }, actions: assign({ discountCode: (_, event) => event.code, }), }, // Kombinierter Guard mit and() + or() SUBMIT: [ { // Pfad 1: Erwachsene + Alkohol → Altersverifikation nötig guard: and(['isAdultContent', not('isAgeVerified')]), target: 'ageVerification', }, { // Pfad 2: Alle Bedingungen erfüllt → direkt zur Zahlung guard: and([ 'isAuthenticated', 'hasValidPayment', 'hasStock', or(['isMinOrder', 'hasFreeShipping']), ]), target: 'processing', }, { // Fallback → Login nötig target: 'requiresAuth', }, ], }, }, ageVerification: { on: { CONFIRM_AGE: { target: 'processing' }, }, }, requiresAuth: {}, processing: {}, }, }, { guards: { // String-Guards: Im guards-Objekt definiert isAuthenticated: (ctx) => ctx.isAuthenticated, hasValidPayment: (ctx) => ctx.hasValidPayment, hasStock: (ctx) => ctx.stockAvailable, isAgeVerified: (ctx) => ctx.userAge >= 18, isAdultContent: () => true, // Produktkategorie-Check // Parametrisierter Guard isMinOrder: (ctx) => ctx.cartTotal >= 15, hasFreeShipping: (ctx) => ctx.cartTotal >= 49, }, } );

Guards mit Claude Code optimieren

Claude Code hilft nicht nur beim Schreiben der Guards — es analysiert auch deren Reihenfolge. Bei mehreren Übergängen auf demselben Event (Array-Syntax) werden Guards sequenziell geprüft. Falsche Reihenfolge führt zu Bugs, die schwer zu debuggen sind. Prompt: "Überprüfe die Guard-Reihenfolge meiner SUBMIT-Transition auf Korrektheit und Vollständigkeit."

💡 Claude Code Tipp: Beschreibe Guards in natürlicher Sprache: "Der Nutzer darf nur weiter, wenn er eingeloggt ist, eine Zahlungsmethode hinterlegt hat und der Warenkorb mindestens €15 beträgt." Claude Code übersetzt das direkt in typensichere Guard-Funktionen.

3. Actions & Effects

Actions sind Seiteneffekte, die bei Transitionen oder beim Ein-/Austreten von States ausgeführt werden. XState v5 unterscheidet zwischen entry actions (beim Eintreten), exit actions (beim Verlassen) und transition actions (während der Transition). Claude Code generiert präzise typisierte Actions mit assign, sendTo, raise und pure.

Actions

Action-Typen in XState v5

import { createMachine, assign, sendTo, raise, log } from 'xstate'; interface FormContext { values: Record<string, string>; errors: Record<string, string>; isDirty: boolean; submissionCount: number; lastSavedAt: number | null; } type FormEvent = | { type: 'CHANGE'; field: string; value: string } | { type: 'BLUR'; field: string } | { type: 'SUBMIT' } | { type: 'VALIDATION_DONE'; errors: Record<string, string> } | { type: 'SAVE_SUCCESS' } | { type: 'SAVE_ERROR'; reason: string }; const formMachine = createMachine( { id: 'form', initial: 'editing', context: { values: {}, errors: {}, isDirty: false, submissionCount: 0, lastSavedAt: null, } satisfies FormContext, states: { editing: { // Entry: Reset-Fehlermeldungen beim Bearbeiten-Modus entry: [ log('Form editing mode entered'), assign({ errors: {} }), ], // Exit: Dirty-Flag setzen wenn wir editing verlassen exit: assign({ isDirty: true }), on: { CHANGE: { actions: [ // assign mit Updater-Funktion (XState v5) assign({ values: ({ context, event }) => ({ ...context.values, [event.field]: event.value, }), }), // Inline-Action als Funktion ({ context, event }) => { console.log(`Field "${event.field}" changed to "${event.value}"`); }, ], }, BLUR: { // Validierung bei blur via raise → intern weiterleiten actions: raise(({ event }) => ({ type: 'VALIDATE_FIELD' as const, field: event.field, })), }, SUBMIT: { target: 'validating', actions: assign({ submissionCount: ({ context }) => context.submissionCount + 1, }), }, }, }, validating: { // Entry: Externe Validierung triggern entry: [ log('Starting validation...'), // Custom Action: Analytics-Event senden ({ context }) => { window.analytics?.track('form_validation_started', { fields: Object.keys(context.values), attempt: context.submissionCount, }); }, ], on: { VALIDATION_DONE: [ { guard: ({ event }) => Object.keys(event.errors).length === 0, target: 'submitting', }, { target: 'editing', actions: assign({ errors: ({ event }) => event.errors, }), }, ], }, }, submitting: { entry: log('Submitting form...'), on: { SAVE_SUCCESS: { target: 'saved', actions: [ assign({ lastSavedAt: () => Date.now(), isDirty: false, }), // sendTo: Event an anderen Actor senden sendTo('notificationActor', { type: 'SHOW_SUCCESS', message: 'Formular gespeichert!', }), ], }, SAVE_ERROR: { target: 'editing', actions: assign({ errors: ({ event }) => ({ _global: event.reason, }), }), }, }, }, saved: { after: { 3000: { target: 'editing' }, // Automatisch zurück nach 3s }, }, }, }, { actions: {}, // Externe Actions können hier registriert werden } );

Claude Code erkennt häufige Action-Muster und schlägt die richtige API vor. Besonders hilfreich ist die automatische TypeScript-Inferenz: Bei assign mit Updater-Funktionen inferiert Claude Code die korrekten Typen aus dem context-Parameter, ohne dass man explizit casten muss.

💡 Entry/Exit vs. Transition Actions: Entry/Exit Actions eignen sich für State-bezogene Seiteneffekte (Analytics, Logging, Setup/Teardown). Transition Actions sind besser für Event-bezogene Mutations (assign mit Event-Daten). Claude Code erklärt den Unterschied auf Nachfrage.

4. Delays & After-Transitions

Zeitgesteuerte Transitionen sind ein häufiger Anwendungsfall: Debounce-Suche, automatisches Ausblenden von Notifications, Session-Timeouts, Retry-Delays mit Exponential Backoff. XState's after-Syntax macht diese Patterns deklarativ und testbar — kein setTimeout im Komponentencode mehr.

Delays

Debounce-Search-Machine

Klassischer Anwendungsfall: Suche erst nach 300ms Inaktivität ausführen, nicht bei jedem Tastendruck.

import { createMachine, assign } from 'xstate'; interface SearchContext { query: string; results: string[]; error: string | null; retryCount: number; } type SearchEvent = | { type: 'TYPE'; query: string } | { type: 'CLEAR' } | { type: 'RESULTS'; data: string[] } | { type: 'ERROR'; message: string }; export const searchMachine = createMachine( { id: 'search', initial: 'idle', context: { query: '', results: [], error: null, retryCount: 0, } satisfies SearchContext, states: { idle: { on: { TYPE: { target: 'debouncing', actions: assign({ query: ({ event }) => event.query }), }, }, }, debouncing: { // after: Automatische Transition nach Delay after: { // Fester Delay: 300ms Debounce DEBOUNCE_DELAY: { target: 'searching' }, }, on: { // Neuer Tastendruck → Timer zurücksetzen TYPE: { target: 'debouncing', // Re-enter → after-Timer startet neu! actions: assign({ query: ({ event }) => event.query }), }, CLEAR: { target: 'idle', actions: assign({ query: '', results: [] }) }, }, }, searching: { entry: assign({ error: null }), on: { RESULTS: { target: 'results', actions: assign({ results: ({ event }) => event.data, retryCount: 0, }), }, ERROR: { target: 'error', actions: assign({ error: ({ event }) => event.message }), }, // Neue Eingabe während Suche → zurück zu debouncing TYPE: { target: 'debouncing', actions: assign({ query: ({ event }) => event.query }), }, }, }, results: { on: { TYPE: { target: 'debouncing', actions: assign({ query: ({ event }) => event.query }), }, CLEAR: { target: 'idle', actions: assign({ query: '', results: [] }) }, }, }, error: { // Exponential Backoff: Retry-Delay verdoppelt sich after: { RETRY_DELAY: { target: 'searching', guard: ({ context }) => context.retryCount < 3, actions: assign({ retryCount: ({ context }) => context.retryCount + 1 }), }, }, on: { TYPE: { target: 'debouncing', actions: assign({ query: ({ event }) => event.query }) }, CLEAR: { target: 'idle', actions: assign({ query: '', results: [], error: null }) }, }, }, }, }, { delays: { // Delays können als Funktionen definiert werden → dynamisch! DEBOUNCE_DELAY: 300, RETRY_DELAY: ({ context }) => { // Exponential Backoff: 1s, 2s, 4s return 1000 * Math.pow(2, context.retryCount); }, }, } );

Session-Timeout-Pattern

// Session-Timeout Machine mit After-Transitions const sessionMachine = createMachine({ id: 'session', initial: 'active', context: { warningShown: false }, states: { active: { after: { // Nach 25 Minuten → Warnung zeigen 1_500_000: { target: 'warning' }, }, on: { ACTIVITY: { target: 'active' }, // Re-enter → Timer zurücksetzen }, }, warning: { entry: assign({ warningShown: true }), after: { // Nach weiteren 5 Minuten → automatisch ausloggen 300_000: { target: 'expired' }, }, on: { EXTEND: { target: 'active' }, LOGOUT: { target: 'expired' }, }, }, expired: { type: 'final', entry: () => window.location.href = '/logout', }, }, });
💡 Testbarkeit von Delays: Da Delays in der Machine-Definition stecken (nicht in setTimeout-Aufrufen), können sie in Tests einfach überschrieben werden: createMachine({...}, { delays: { DEBOUNCE_DELAY: 0 } }). Claude Code schlägt das automatisch vor, wenn du nach Tests fragst.

5. Actor Model & Parallel States

Das Actor Model ist das Herzstück von XState v5. Jede Machine läuft als Actor — eine isolierte Einheit mit eigenem State, Mailbox und Lebenszyklus. Actors können andere Actors spawnen, Nachrichten senden und empfangen. Parallel States (früher "parallel machines") ermöglichen gleichzeitige Zustandsregionen.

Actor Model

Dashboard mit Parallel States

Ein Dashboard hat gleichzeitig mehrere unabhängige Bereiche: Datenladen, UI-State, Notification-System. Mit type: 'parallel' modellieren wir das sauber.

import { createMachine, createActor, assign, sendTo, spawn } from 'xstate'; // Child Actor: Notification-System const notificationMachine = createMachine({ id: 'notifications', initial: 'empty', context: { messages: [] as Array<{ id: string; text: string; type: 'info' | 'error' | 'success' }>, }, states: { empty: { on: { ADD: { target: 'showing', actions: assign({ messages: ({ context, event }) => [ ...context.messages, { id: crypto.randomUUID(), text: event.text, type: event.notifType }, ], }), }, }, }, showing: { on: { ADD: { actions: assign({ messages: ({ context, event }) => [ ...context.messages, { id: crypto.randomUUID(), text: event.text, type: event.notifType }, ], }), }, DISMISS: { actions: assign({ messages: ({ context, event }) => context.messages.filter((m) => m.id !== event.id), }), }, }, }, }, }); // Parent Actor: Dashboard mit Parallel States const dashboardMachine = createMachine({ id: 'dashboard', // type: 'parallel' → alle Regions laufen gleichzeitig! type: 'parallel', context: { data: null as null | Record<string, unknown>, sidebarOpen: true, theme: 'light' as 'light' | 'dark', notificationRef: null as null | ReturnType<typeof spawn>, }, states: { // Region 1: Daten-Loading dataRegion: { initial: 'loading', entry: assign({ // Notification-Actor spawnen beim Start notificationRef: () => spawn(notificationMachine, { id: 'notifications' }), }), states: { loading: { invoke: { src: 'loadDashboardData', onDone: { target: 'loaded', actions: [ assign({ data: ({ event }) => event.output }), sendTo('notifications', { type: 'ADD', text: 'Dashboard geladen', notifType: 'success', }), ], }, onError: { target: 'error', actions: sendTo('notifications', ({ event }) => ({ type: 'ADD', text: `Fehler: ${event.error}`, notifType: 'error', })), }, }, }, loaded: { on: { REFRESH: { target: 'loading', actions: assign({ data: null }) }, }, }, error: { on: { RETRY: 'loading' }, }, }, }, // Region 2: UI-State (unabhängig von Daten) uiRegion: { initial: 'normal', states: { normal: { on: { TOGGLE_SIDEBAR: { actions: assign({ sidebarOpen: ({ context }) => !context.sidebarOpen, }), }, TOGGLE_THEME: { actions: assign({ theme: ({ context }) => context.theme === 'light' ? 'dark' : 'light', }), }, OPEN_SETTINGS: { target: 'settings' }, }, }, settings: { on: { CLOSE_SETTINGS: { target: 'normal' }, }, }, }, }, }, }); // Actor erstellen und starten const dashboardActor = createActor(dashboardMachine); dashboardActor.start(); // Events senden dashboardActor.send({ type: 'TOGGLE_THEME' }); dashboardActor.send({ type: 'REFRESH' }); // State abonnieren dashboardActor.subscribe((snapshot) => { console.log('Data region:', snapshot.value.dataRegion); console.log('UI region:', snapshot.value.uiRegion); });
⚠️ Parallel States !== Multi-Threading: Parallele Regions laufen nicht wirklich parallel (JavaScript ist single-threaded). Sie sind unabhängige State-Maschinen, die gleichzeitig aktiv sein können. Claude Code hilft, die richtige Granularität zu finden.

6. React Integration & Visualizer

@xstate/react v4 bietet drei Hooks: useMachine für lokale Machines, useActorRef für Actors aus externem Kontext und useSelector für performante Teil-Subscriptions. Claude Code generiert React-Komponenten, die exakt den richtigen Hook für den jeweiligen Anwendungsfall nutzen.

React

useMachine — Lokale State Machine

useMachine eignet sich für State Machines, die nur in einer Komponente genutzt werden.

import { useMachine, useSelector, useActorRef } from '@xstate/react'; import { createActorContext } from '@xstate/react'; import React, { useCallback } from 'react'; import { searchMachine } from './searchMachine'; import { dashboardMachine } from './dashboardMachine'; // ---- 1. useMachine: Lokale Machine ---- export function SearchComponent() { const [state, send] = useMachine(searchMachine); const handleChange = useCallback( (e: React.ChangeEvent<HTMLInputElement>) => { send({ type: 'TYPE', query: e.target.value }); }, [send] ); return ( <div> <input type="text" value={state.context.query} onChange={handleChange} placeholder="Suchen..." /> {/* State-basiertes Rendering */} {state.matches('debouncing') && <span>Warte...</span>} {state.matches('searching') && <span>Suche läuft...</span>} {state.matches('error') && ( <div className="error">{state.context.error}</div> )} {state.matches('results') && ( <ul> {state.context.results.map((result) => ( <li key={result}>{result}</li> ))} </ul> )} </div> ); } // ---- 2. createActorContext: Shared Actor über Context ---- const DashboardContext = createActorContext(dashboardMachine); export function DashboardProvider({ children }: { children: React.ReactNode }) { return ( <DashboardContext.Provider> {children} </DashboardContext.Provider> ); } // ---- 3. useSelector: Performance-optimierte Subscriptions ---- function ThemeToggle() { const actorRef = DashboardContext.useActorRef(); // Nur re-rendern wenn theme sich ändert (nicht bei jedem State-Update) const theme = DashboardContext.useSelector( (snapshot) => snapshot.context.theme ); return ( <button onClick={() => actorRef.send({ type: 'TOGGLE_THEME' })}> {theme === 'light' ? '🌙 Dark Mode' : '☀️ Light Mode'} </button> ); } // ---- 4. useActorRef: Direkter Zugriff auf den Actor ---- function DataPanel() { const isLoading = DashboardContext.useSelector( (s) => s.matches({ dataRegion: 'loading' }) ); const data = DashboardContext.useSelector((s) => s.context.data); const actorRef = DashboardContext.useActorRef(); return ( <div> {isLoading ? ( <p>Daten werden geladen...</p> ) : ( <> <pre>{JSON.stringify(data, null, 2)}</pre> <button onClick={() => actorRef.send({ type: 'REFRESH' })}> Aktualisieren </button> </> )} </div> ); } // ---- 5. Stately Visualizer (Dev-Only) ---- if (process.env.NODE_ENV === 'development') { import('@statelyai/inspect').then(({ createBrowserInspector }) => { const inspector = createBrowserInspector(); // Inspector an Machine übergeben: // const actor = createActor(machine, { inspect: inspector.inspect }); // actor.start(); inspector.inspect; }); }

DevTools & Stately Visualizer

Der Stately Visualizer ist ein unverzichtbares Debugging-Tool für XState. Er zeigt den aktuellen State, alle möglichen Transitionen und den Context in Echtzeit. Claude Code hilft beim Setup und erklärt, welche Transitionen fehlen oder unnötig sind — besonders nützlich bei komplexen Parallel-State-Machines.

// Visualizer in der App aktivieren (Dev-Only) import { createBrowserInspector } from '@statelyai/inspect'; import { createActor } from 'xstate'; const inspector = createBrowserInspector({ autoStart: true, // Öffnet automatisch in neuem Tab }); const actor = createActor(dashboardMachine, { inspect: inspector.inspect, }); actor.start(); // In React mit createActorContext: const DashboardCtx = createActorContext(dashboardMachine, { inspect: process.env.NODE_ENV === 'development' ? createBrowserInspector().inspect : undefined, });
Best Practices

Claude Code Workflow für XState-Projekte

  1. Machine zuerst beschreiben: "Ich brauche eine Machine für einen mehrstufigen Wizard mit 4 Schritten und optionalem Schritt 3."
  2. States und Events iterativ verfeinern: Claude Code erkennt fehlende Transitionen und Sackgassen-States
  3. TypeScript-Typen generieren lassen: "Erstelle vollständige TypeScript-Typen für Context und Events"
  4. Guards und Actions nachfordern: "Füge Guards für die Validierung und Analytics-Actions hinzu"
  5. React-Integration: "Welcher Hook passt für meinen Anwendungsfall?"
  6. Tests generieren: "Schreibe Vitest-Tests für alle Transitionen der Machine"

Testing-Pattern mit Vitest

import { describe, it, expect } from 'vitest'; import { createActor } from 'xstate'; import { searchMachine } from './searchMachine'; describe('searchMachine', () => { it('startet im idle-State', () => { const actor = createActor(searchMachine); actor.start(); expect(actor.getSnapshot().matches('idle')).toBe(true); actor.stop(); }); it('geht bei TYPE zu debouncing', () => { const actor = createActor(searchMachine); actor.start(); actor.send({ type: 'TYPE', query: 'test' }); expect(actor.getSnapshot().matches('debouncing')).toBe(true); expect(actor.getSnapshot().context.query).toBe('test'); actor.stop(); }); it('debounced Suche: Timer-Reset bei erneutem TYPE', async () => { const actor = createActor(searchMachine, { // Delays für Tests auf 0 setzen input: {}, }); actor.start(); actor.send({ type: 'TYPE', query: 'hel' }); actor.send({ type: 'TYPE', query: 'hell' }); actor.send({ type: 'TYPE', query: 'hello' }); // Immer noch debouncing nach mehrfachem TYPE expect(actor.getSnapshot().context.query).toBe('hello'); actor.stop(); }); it('CLEAR setzt State und Query zurück', () => { const actor = createActor(searchMachine); actor.start(); actor.send({ type: 'TYPE', query: 'test' }); actor.send({ type: 'CLEAR' }); expect(actor.getSnapshot().matches('idle')).toBe(true); expect(actor.getSnapshot().context.query).toBe(''); actor.stop(); }); });

Fazit: XState + Claude Code = elegante UI-Zustände

XState v5 und Claude Code ergänzen sich hervorragend: Claude Code versteht State-Machine-Konzepte auf konzeptioneller Ebene und übersetzt natürlichsprachige Beschreibungen in typensicheren TypeScript-Code. Das Ergebnis sind UIs, die vorhersehbar, testbar und wartbar sind — auch wenn die Zustandslogik komplex wird.

Die wichtigsten Vorteile im Überblick:

Starte mit einer einfachen Fetch-Machine, erweitere sie mit Guards und Actions, und nutze den Visualizer zum Debuggen. Claude Code führt dich durch jeden Schritt — von der Konzeption bis zur vollständig typisierten React-Integration.

State-Patterns-Modul im Kurs

Im Claude Code Mastery Kurs: vollständiges XState-Modul mit Actors, Guards, Delays, Parallel States und React-Integration für komplexe UI-Zustände.

14 Tage kostenlos testen →