Zum Inhalt springen

Formularabläufe mit Zustandsautomaten

12-stufiges Formular. Bedingte Validierung. Die Schaltfläche „Zurück“ muss funktionieren.

Einen öffentlichen Ablauf zur Investorenaufnahme mit XState, elf Validierungs-Guards, erhaltenem Formularkontext und Tests auf Browserebene modellieren.

6 Min. LesezeitVon

Die Übergänge waren Teil des Produkts

Ein Schrittzähler genügte nur am Anfang. In Schritt fünf erfasste der Ablauf Position, gesammelte Daten, Validierungsfehler, Ladezustand, abgeschlossene Schritte und bedingte Anforderungen. In Schritt zehn umfasste eine Komponente etwa 800 Zeilen, und die Frage „Kann dieser Nutzer fortfahren?“ wurde an sechs verschiedenen Stellen beantwortet.

Zwölf Bildschirme darzustellen war unkompliziert. Vorwärts-, Rückwärts-, asynchrone und Übermittlungsübergänge konsistent zu halten, ohne frühere Eingaben zu verlieren, war es nicht.

Auch geordneter Zustand kann unmögliche Zustände zulassen

useState ließ das Formular Werte kombinieren, die nicht gleichzeitig bestehen sollten: einen späteren Bildschirm, nachdem erforderliche vorherige Daten gelöscht worden waren, einen Rückwärtsübergang während der Übermittlung oder eine aktivierte Schaltfläche bei leerem sichtbarem Bedingungsfeld. useReducer konnte Aktualisierungen zentralisieren, hätte aber weiterhin aus jedem aktuellen Schritt jede im Reducer implementierte Aktion akzeptiert, sofern nicht jeder Zweig einen Übergangsgraphen nachbildete.

XState v5 mit @xstate/react bildete diesen Graphen ausdrücklich ab. Ein Ereignis kann weiterhin gesendet werden, aber ein nicht definierter oder von einem Guard abgelehnter Übergang bewegt den Automaten nicht.

Den Ablaufvertrag ausdrücklich festlegen

Der Automat nutzte sieben Ereignisse: NEXT, PREVIOUS, GO_TO_STEP, UPDATE_FORM_DATA, SET_CREATED_BUYER_ID, SUBMIT und RESET. Der Kontext enthielt currentStep, formData, errors, isLoading, investorId und createdBuyerId.

CONTACT_PREFERENCES: {
  on: {
    NEXT: {
      target: 'TARGET_MARKETS',
      guard: 'CAN_PROGRESS_FROM_CONTACT_PREFERENCES'
    },
    PREVIOUS: 'INTRO',
    UPDATE_FORM_DATA: { actions: 'UPDATE_FORM_DATA' }
  }
}

NEXT war durch einen Guard geschützt. PREVIOUS blieb verfügbar, solange keine Übermittlung lief, damit ein Nutzer frühere Daten korrigieren konnte, ohne den Formularkontext zurückzusetzen. UPDATE_FORM_DATA führte nur die geänderten Felder mit dem gesammelten Kontext zusammen.

Validierung an der Übergangsgrenze platzieren

Elf Guards folgten der Konvention CAN_PROGRESS_FROM_[STEP_NAME]. Ein Zod-Schema verlangte mindestens eine Telefonnummer oder E-Mail-Adresse und validierte eine US-Telefonnummer nur, wenn sie vorhanden war. Zielmärkte, Immobilientypen und Strategien erforderten jeweils eine Auswahl; Bildschirme für Wertebereiche verlangten mindestens ein Minimum oder Maximum.

const canGoNext = state.can({ type: "NEXT" })

CAN_PROGRESS_FROM_CONTACT_CARD: ({ context }) =>
  Boolean(context.createdBuyerId)

Der Guard für Contact Card gab ursprünglich bedingungslos true zurück. Eine spätere Korrektur band ihn an createdBuyerId, sodass der Automat diesen Zustand erst verlassen konnte, nachdem die Käufererstellung im Backend erfolgreich war. Derselbe Vertrag state.can({ type: "NEXT" }) steuerte den Schaltflächenzustand und den Übergang selbst.

Wettlaufsituationen zwischen asynchronen Prüfungen und dem Formular vermeiden

Das Formular integrierte außerdem generierte APIs zur Kontaktprüfung und Käufererstellung. Telefon- und E-Mail-Prüfungen wurden entprellt, und eine monoton steigende Anfrage-ID verwarf eine veraltete Antwort, wenn bereits ein neuerer Wert eingegeben worden war. Zielmarktempfehlungen nutzten eine koordinatenbasierte Deduplizierung, bevor sie in den Automatenkontext gelangten.

Die Käufererstellung erfolgte am Übergang der Contact Card. SET_CREATED_BUYER_ID speicherte die bestätigte Backend-ID im Automatenkontext; spätere Einstellungsbildschirme nutzten diese ID für ihre API-Aktualisierungen.

React-Komponenten müssen den Routengraphen nicht kennen

Ein Provider umschloss useMachine und stellte currentStep, formData, canGoNext, isSubmitting, handleNext, handlePrevious, handleUpdateFormData und die Käufer-ID-Aktion bereit. Schrittkomponenten renderten ihre Felder und sendeten Aktualisierungen, ohne zu wissen, welche Route davor oder danach lag.

Der Automat steuert die Navigation. Jeder React-Schritt rendert Felder, aktualisiert den Kontext und sendet Ereignisse. Für ein dreistufiges Formular würde ich XState nicht einsetzen; bei zwölf Bildschirmen, bedingter Validierung und backendabhängigen Übergängen rechtfertigte der ausdrückliche Graph die zusätzliche Konfiguration.

Regeln getrennt von den Bildschirmen prüfen

Die Implementierung ergänzte 538 Zeilen Unit-Abdeckung für Schemas und direkte Übergänge. Diese Tests sendeten Ereignisse an den Automaten und prüften den Zustand, ohne React-Komponenten einzubinden. Weitere 359 Zeilen Playwright-Abdeckung prüften den vollständigen Ablauf, Telefon- und E-Mail-Regeln, die Käufererstellung im Backend und den Datenerhalt bei Rückwärtsnavigation.

  • 12 Schritte
  • sieben Ereignisse
  • 11 benannte Vorwärts-Guards
  • eine Automatendatei zur Steuerung der Navigation
  • Schema- und Automatentests getrennt von Browserinteraktionstests