Gå til innhold

Skjemaflyter med tilstandsmaskiner

Skjema med 12 trinn. Betinget validering. Tilbake-knappen må virke.

Modellering av en offentlig Investor Intake-flyt med XState, elleve valideringsvakter, bevart skjemakontekst og tester på nettlesernivå.

6 min lesetidAv

Overgangene var en del av produktet

En enkel teller for skjematrinn var bare nok i begynnelsen. Ved trinn fem holdt flyten rede på posisjon, oppsamlede data, valideringsfeil, lasting, fullførte trinn og betingede krav. Ved trinn ti hadde én komponent nådd rundt 800 linjer, og spørsmålet «kan denne brukeren fortsette?» ble besvart seks forskjellige steder.

Det var enkelt å vise tolv skjermbilder. Det var ikke enkelt å holde overganger fremover, bakover, ved asynkront arbeid og ved innsending konsistente uten å miste tidligere inndata.

Strukturert tilstand kan fortsatt tillate umulige kombinasjoner

useState lot skjemaet kombinere verdier som ikke burde kunne finnes samtidig: et senere skjermbilde etter at nødvendige data var slettet, en overgang bakover under innsending eller en aktivert knapp mens et synlig, betinget felt var tomt. useReducer kunne samle oppdateringene, men ville fremdeles godta enhver handling som reduceren implementerte, fra ethvert gjeldende trinn, med mindre hver gren gjenskapte en overgangsgraf.

XState v5 med @xstate/react gjorde denne grafen uttrykkelig. En hendelse kan fortsatt sendes, men en udefinert overgang eller en overgang som avvises av en vakt, flytter ikke maskinen.

Gjør arbeidsflytkontrakten eksplisitt

Maskinen brukte syv hendelser: NEXT, PREVIOUS, GO_TO_STEP, UPDATE_FORM_DATA, SET_CREATED_BUYER_ID, SUBMIT og RESET. Konteksten inneholdt currentStep, formData, errors, isLoading, investorId og createdBuyerId.

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

NEXT var beskyttet av en vakt. PREVIOUS var tilgjengelig utenom innsending, slik at brukeren kunne rette tidligere data uten å nullstille skjemakonteksten. UPDATE_FORM_DATA slo bare de endrede feltene sammen med den oppsamlede konteksten.

Sett validering ved overgangsgrensen

Elleve vakter fulgte navnekonvensjonen CAN_PROGRESS_FROM_[STEP_NAME]. Et Zod-skjema krevde minst ett av følgende: et telefonnummer eller en e-postadresse. Et amerikansk telefonnummer ble bare validert når det var oppgitt. Målmarkeder, eiendomstyper og strategier krevde hvert sitt valg. Skjermbilder for intervaller krevde enten en minste- eller en høyesteverdi.

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

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

Vakten for Contact Card returnerte opprinnelig alltid true. En senere rettelse knyttet den til createdBuyerId, slik at maskinen ikke kunne forlate tilstanden før kjøperen var opprettet på tjenersiden. Den samme kontrakten, state.can({ type: "NEXT" }), styrte både knappens tilstand og selve overgangen.

Hindre asynkron verifisering i å havne i kappløp med skjemaet

Skjemaet integrerte også genererte API-klienter for kontaktverifisering og oppretting av kjøpere. Kontrollene av telefon og e-post ventet til brukeren tok en skrivepause, og en monotont økende forespørsels-ID forkastet et foreldet svar dersom en nyere verdi allerede var skrevet inn. Anbefalinger av målmarkeder fjernet duplikater ut fra koordinater før de ble lagt i maskinkonteksten.

Kjøperen ble opprettet ved grensen til Contact Card. SET_CREATED_BUYER_ID lagret den bekreftede ID-en fra tjenersiden i maskinkonteksten. Senere skjermbilder for innstillinger brukte denne ID-en i API-oppdateringene sine.

La React-komponentene slippe å kjenne rutegrafen

En provider pakket inn useMachine og eksponerte currentStep, formData, canGoNext, isSubmitting, handleNext, handlePrevious, handleUpdateFormData og handlingen for kjøper-ID. Trinnkomponentene viste feltene sine og sendte oppdateringer uten å vite hvilken rute som kom før eller etter.

Maskinen eier navigasjonen. Hvert React-trinn viser felt, oppdaterer kontekst og sender hendelser. Jeg ville ikke brukt XState til et skjema med tre trinn. Tolv skjermbilder, betinget validering og overganger som var avhengige av tjenersiden, gjorde den uttrykkelige grafen verdt den ekstra konfigurasjonen her.

Test reglene uavhengig av skjermene

Implementasjonen la til 538 linjer med enhetstester for skjemaer og direkte overganger. Testene sendte hendelser til maskinen og kontrollerte tilstanden uten å montere React-komponenter. Ytterligere 359 linjer med Playwright-tester dekket hele flyten, reglene for telefon og e-post, oppretting av kjøperen på tjenersiden og bevaring av data ved tilbakenavigasjon.

  • 12 trinn
  • syv hendelser
  • 11 navngitte vakter for fremoverbevegelse
  • én maskinfil som kontrollerer navigasjonen
  • skjema- og maskintester adskilt fra tester av samhandling i nettleseren