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
- String-Guards: Im
guards-Objekt referenzierte Funktionen
- Inline Guards: Direkt als Funktion in der Transition definiert
- Parametrisierte Guards: Guards mit zusätzlichen Konfigurationswerten
- Kombinierte Guards:
and(), or(), not() aus XState
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
assign — Context aktualisieren (einzige reine Context-Mutation)
sendTo — Event an anderen Actor senden
raise — Event an sich selbst senden (intern)
log — Logging (Dev-Hilfe)
- Custom Actions — Beliebige Funktionen mit Seiteneffekten
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
- Machine zuerst beschreiben: "Ich brauche eine Machine für einen mehrstufigen Wizard mit 4 Schritten und optionalem Schritt 3."
- States und Events iterativ verfeinern: Claude Code erkennt fehlende Transitionen und Sackgassen-States
- TypeScript-Typen generieren lassen: "Erstelle vollständige TypeScript-Typen für Context und Events"
- Guards und Actions nachfordern: "Füge Guards für die Validierung und Analytics-Actions hinzu"
- React-Integration: "Welcher Hook passt für meinen Anwendungsfall?"
- 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:
- Kein impliziter State: Alle Zustände sind explizit benannt — kein
isLoading && !isError && hasData mehr
- Unmögliche States ausgeschlossen: Die Machine kann nicht gleichzeitig
loading und error sein
- Testbar ohne React: Machine-Logik ist unabhängig von der UI testbar
- Visualisierbar: Stately Visualizer zeigt alle States und Transitionen grafisch
- Claude Code versteht Muster: Guards, Actions, Delays und Actors auf Prompt-Ebene erklärbar
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 →