Skip to content

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 context
interface FormContext {
// Context would store form data and validation state
}
// Define events
type FormEvent = { type: "SUBMIT"; value: string } | { type: "RESET" };
// Create machine
const 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",
},
},
},
});
// Usage
let state = formMachine.initialState;
console.log(state); // 'idle'
// Submit with invalid input
state = 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 input
state = 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

  1. Guards (conditional transitions) - The cond property defines a function that determines which transition to take. If the condition returns true, that transition is used; otherwise, the next transition in the array is tried.

  2. Event payloads - Events can carry data (value: string), allowing validation logic to inspect the actual input being validated.

  3. Multiple transitions per event - When an event can lead to different states based on conditions, use an array of transition objects with guards.

  4. 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 in invalid)

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 validating into checkingFormat, checkingUniqueness, etc.
  • History states: Return to the last valid/invalid state after interruption

Next Steps