Form Validation State Machine
Managing form state with validation logic using guards and conditional transitions.
Use Case
This example demonstrates a form with idle β validating β valid/invalid states. Itβs applicable to:
- Login and signup forms with validation
- Multi-step form wizards
- Data entry forms with complex validation rules
- Any UI requiring state-driven validation feedback
Complete Code
import { setup } from "xstate";
// Define contextinterface FormContext { // Context would store form data and validation state}
// Define eventstype FormEvent = { type: "SUBMIT"; value: string } | { type: "RESET" };
// Create machineconst formMachine = setup({ types: { context: {} as FormContext, events: {} as FormEvent, },}).createMachine({ id: "form", initial: "idle", states: { idle: { on: { SUBMIT: "validating", }, }, validating: { on: { SUBMIT: [ { target: "valid", cond: (event) => event.value.length >= 3, }, { target: "invalid", }, ], }, }, valid: { on: { RESET: "idle", SUBMIT: "validating", }, }, invalid: { on: { RESET: "idle", SUBMIT: "validating", }, }, },});
// Usagelet state = formMachine.initialState;console.log(state); // 'idle'
// Submit with invalid inputstate = formMachine.transition(state, { type: "SUBMIT", value: "ab" });console.log(state); // 'validating'
state = formMachine.transition(state, { type: "SUBMIT", value: "ab" });console.log(state); // 'invalid' (value too short)
// Reset and try valid inputstate = formMachine.transition(state, { type: "RESET" });console.log(state); // 'idle'
state = formMachine.transition(state, { type: "SUBMIT", value: "abc" });console.log(state); // 'validating'
state = formMachine.transition(state, { type: "SUBMIT", value: "abc" });console.log(state); // 'valid' (meets minimum length)Code Explanation
-
Guards (conditional transitions) - The
condproperty defines a function that determines which transition to take. If the condition returnstrue, that transition is used; otherwise, the next transition in the array is tried. -
Event payloads - Events can carry data (
value: string), allowing validation logic to inspect the actual input being validated. -
Multiple transitions per event - When an event can lead to different states based on conditions, use an array of transition objects with guards.
-
Validation logic - Business rules (like minimum length) are encoded as guard functions, keeping validation logic co-located with state definitions.
Key Concepts
- Guards: Functions that conditionally allow or prevent transitions based on event data or current state
- Event payloads: Events can carry data beyond just the event type
- Conditional transitions: Array of transitions where the first matching guard is taken
- State-driven UI: UI can render different feedback based on state (loading spinner in
validating, error message ininvalid)
Advanced Pattern: Multiple Validation Rules
const advancedFormMachine = createMachine<FormState, FormEvent>({ id: "advancedForm", initial: "idle", states: { idle: { on: { SUBMIT: "validating" }, }, validating: { on: { SUBMIT: [ { target: "valid", cond: (event) => { const value = event.value; return ( value.length >= 3 && value.length <= 20 && /^[a-zA-Z0-9]+$/.test(value) ); }, }, { target: "invalid" }, ], }, }, valid: { on: { RESET: "idle", SUBMIT: "validating", }, }, invalid: { on: { RESET: "idle", SUBMIT: "validating", }, }, },});Extending for Real Forms
For production forms, you might add:
- Context: Store error messages and validation details
- Actions: Side effects like API calls or analytics tracking
- Nested states: Break down
validatingintocheckingFormat,checkingUniqueness, etc. - History states: Return to the last valid/invalid state after interruption
Next Steps
- Basic State Machine - Review foundational concepts
- Traffic Light Example - See cyclic state transitions
- API Documentation - Learn about context, actions, and nested states
- API Reference - Understand design patterns and best practices