Formulierprocessen met toestandsmachines
Formulier met 12 stappen. Voorwaardelijke validatie. De terugknop moet werken.
Een openbaar intakeproces voor investeerders modelleren met XState, elf validatieguards, behouden formuliercontext en browsertests.
6 min. leestijdDoor Abdullah Raheel
De overgangen maakten deel uit van het product
Een stappenteller volstond alleen aan het begin. Bij stap vijf hield het proces de positie, verzamelde gegevens, validatiefouten, laadstatus, voltooide stappen en voorwaardelijke vereisten bij. Bij stap tien telde één component ongeveer 800 regels en werd de vraag 'kan deze gebruiker verder?' op zes verschillende plaatsen beantwoord.
Twaalf schermen renderen was eenvoudig. Vooruit- en teruggaan, asynchrone verwerking en het indienen consistent houden zonder eerdere invoer te verliezen was dat niet.
Geordende status kan nog steeds onmogelijke toestanden toelaten
useState liet het formulier waarden combineren die niet tegelijk zouden moeten bestaan: een later scherm nadat vereiste gegevens waren gewist, teruggaan tijdens het indienen of een ingeschakelde knop terwijl een zichtbaar voorwaardelijk veld leeg was. useReducer kon updates centraliseren, maar zou nog steeds elke door de reducer geïmplementeerde actie vanuit elke huidige stap accepteren, tenzij elke tak opnieuw een overgangsgrafiek bouwde.
XState v5 met @xstate/react maakte die grafiek expliciet. Een gebeurtenis kan nog steeds worden verstuurd, maar een niet-gedefinieerde of door een guard geweigerde overgang verplaatst de machine niet.
Maak het workflowcontract expliciet
De machine gebruikte zeven gebeurtenissen: NEXT, PREVIOUS, GO_TO_STEP, UPDATE_FORM_DATA, SET_CREATED_BUYER_ID, SUBMIT en RESET. De context bevatte currentStep, formData, errors, isLoading, investorId en createdBuyerId.
CONTACT_PREFERENCES: {
on: {
NEXT: {
target: 'TARGET_MARKETS',
guard: 'CAN_PROGRESS_FROM_CONTACT_PREFERENCES'
},
PREVIOUS: 'INTRO',
UPDATE_FORM_DATA: { actions: 'UPDATE_FORM_DATA' }
}
}NEXT werd door een guard beschermd. PREVIOUS bleef beschikbaar wanneer het formulier niet werd ingediend, zodat een gebruiker eerdere gegevens kon corrigeren zonder de formuliercontext te wissen. UPDATE_FORM_DATA voegde alleen de gewijzigde velden samen met de opgebouwde context.
Plaats validatie op de overgangsgrens
Elf guards volgden de conventie CAN_PROGRESS_FROM_[STEP_NAME]. Een Zod-schema vereiste ten minste een telefoonnummer of e-mailadres en valideerde een Amerikaans telefoonnummer alleen wanneer het aanwezig was. Doelmarkten, vastgoedtypen en strategieën vereisten elk een selectie; bereikschermen vereisten ten minste een minimum of maximum.
const canGoNext = state.can({ type: "NEXT" })
CAN_PROGRESS_FROM_CONTACT_CARD: ({ context }) =>
Boolean(context.createdBuyerId)De Contact Card-guard retourneerde oorspronkelijk altijd true. Een latere oplossing koppelde deze aan createdBuyerId, zodat de machine die toestand niet kon verlaten voordat het aanmaken van de koper in de backend was geslaagd. Hetzelfde contract state.can({ type: "NEXT" }) stuurde zowel de knopstatus als de overgang aan.
Voorkom raceconditions tussen asynchrone verificatie en het formulier
Het formulier integreerde ook gegenereerde API's voor contactverificatie en het aanmaken van kopers. Telefoon- en e-mailcontroles werden gedebounced en een monotoon oplopende aanvraag-id verwierp een verouderd antwoord wanneer al een nieuwere waarde was ingevoerd. Aanbevelingen voor doelmarkten gebruikten deduplicatie op basis van coördinaten voordat ze de machinecontext ingingen.
De koper werd aangemaakt bij de grens van de Contact Card. SET_CREATED_BUYER_ID sloeg de bevestigde backend-id op in de machinecontext; latere voorkeursschermen gebruikten die id voor hun API-updates.
Houd React-componenten onbewust van de routegrafiek
Een provider verpakte useMachine en stelde currentStep, formData, canGoNext, isSubmitting, handleNext, handlePrevious, handleUpdateFormData en de koper-id-actie beschikbaar. Stapcomponenten renderden hun velden en stuurden updates zonder te weten welke route ervoor of erna kwam.
De machine beheert de navigatie. Elke React-stap rendert velden, werkt de context bij en stuurt gebeurtenissen. Ik zou XState niet gebruiken voor een formulier met drie stappen; twaalf schermen, voorwaardelijke validatie en backendafhankelijke overgangen maakten de expliciete grafiek hier de extra configuratie waard.
Test de regels los van de schermen
De implementatie voegde 538 regels unitdekking toe voor schema's en rechtstreekse overgangen. Die tests stuurden gebeurtenissen naar de machine en controleerden de toestand zonder React-componenten te mounten. Nog eens 359 regels Playwright-dekking testten het volledige proces, de telefoon- en e-mailregels, het aanmaken van kopers in de backend en het behoud van gegevens bij terugnavigatie.
- 12 stappen
- zeven gebeurtenissen
- 11 benoemde guards voor voorwaartse beweging
- één machinebestand dat de navigatie beheert
- schema- en machinetests gescheiden van browserinteractietests

