Building a Generic State Machine for Form Handling Using XState
Tags: xstate, statemachine, statechart, javascript
Published: 2020-03-08 13:32:03

---
title: Building a Generic State Machine for Form Handling Using XState
description: If you're a computer scientist or follow @davidkpiano you've probably heard about state machines. They are awesome. Here's an example of how to use one for form handling.
published: true
tags: [xstate, statemachine, statechart, javascript]
canonical_url: https://blog.robertbroersma.com/building-a-generic-state-machine-for-form-handling-using-xstate
---

If you're a computer scientist or follow @davidkpiano you've probably heard about state machines.

They are awesome.

Here's an example of how to use one for form handling!

Our designer says the form should look like this:

![4 App States: Editing, Submitting, Error, Success](https://dev-to-uploads.s3.amazonaws.com/i/y6ivj1saelpxdxdrr7p9.png)

From this concept we can deduce four "states":

1. Editing
2. Submitting
3. Error
4. Success

Let's define the states in a machine:

```javascript
const formMachine = Machine({
  // We'll start in the editing state
  initial: 'editing',
  states: {
    editing: {},
    submitting: {},
    error: {},
    success: {},
  },
})
```

## Editing State

While in the editing state, we can do 2 things:

- Type in the fields. We stay in the same state. We of course also want the input to be saved.
- Submit the form. We transition to the _submitting_ state.

Let's define the transitions and actions:

```javascript
const formMachine = Machine(
  {
    initial: 'editing',
    // Context contains all our infinite state, like text input!
    context: {
      values: {},
    },
    states: {
      editing: {
        on: {
          CHANGE: {
            // Stay in the same state
            target: '',

            // Execute the onChange action
            actions: ['onChange'],
          },
          SUBMIT: 'submitting',
        },
      },
      submitting: {},
      error: {},
      success: {},
    },
  },
  {
    actions: {
      // Assign
      onChange: assign({
        values: (ctx, e) => ({
          ...ctx.values,
          [e.key]: e.value,
        }),
      }),
    },
  },
)
```

## Submitting State

After submitting the form, our life could go one of two ways:

- The submission is succesful, we move to the _success_ state.
- The submission failed, we move to the _error_ state.

To keep our machine generic, we'll leave the whatever happens during the submission up to the consumer of the machine by invoking a service. Allowing the consumer to pass in their own service (See [Invoking Services](https://xstate.js.org/docs/guides/communication.html)). Be it frontend validation, backend validation or no validation, we don't care! The only thing we'll do is transition based on a succesful or unsuccesful response, storing the error data on an unsuccesful response.

```javascript
const formMachine = Machine(
  {
    initial: 'editing',
    context: {
      values: {},
      errors: {},
    },
    states: {
      editing: {
        on: {
          CHANGE: {
            target: '',
            actions: ['onChange'],
          },
          SUBMIT: 'submitting',
        },
      },
      submitting: {
        invoke: {
          src: 'onSubmit',
          // Move to the success state onDone
          onDone: 'success',
          onError: {
            // Move to the error state onError
            target: 'error',

            // Execute onChange action
            actions: ['onError'],
          },
        },
      },
      error: {},
      success: {},
    },
  },
  {
    actions: {
      onChange: assign({
        values: (ctx, e) => ({
          ...ctx.values,
          [e.key]: e.value,
        }),
      }),
      onError: assign({
        errors: (_ctx, e) => e.data,
      }),
    },
  },
)
```

# Error State

Uh-Oh! We've stumbled upon a few errors. The user can now do two things:

- Change the inputs.
- Submit the form again.

Hey, these are the same things we could do in the _editing_ state! Come to think of it, this state is actually pretty similar to editing, only there are some errors in the screen. We _could_ now move the transitions up to the root state, allowing us to ALWAYS change the inputs and ALWAYS submit the form, but obviously we don't want that! We don't want the user to edit the form _while_ it's submitting. What we _can_ do is make the editing state hierarchical with 2 substates: _pristine_ (not submitted) and _error_ (submitted and wrong):

```javascript
const formMachine = Machine(
  {
    initial: 'editing',
    context: {
      values: {},
      errors: {},
    },
    states: {
      editing: {
        // We start the submachine in the pristine state
        initial: 'pristine',
        // These transitions are available in all substates
        on: {
          CHANGE: {
            actions: ['onChange'],
          },
          SUBMIT: 'submitting',
        },
        // The 2 substates
        states: {
          pristine: {},
          error: {},
        },
      },
      submitting: {
        invoke: {
          src: 'onSubmit',
          onDone: 'success',
          onError: {
            // Note that we now need to point to the error substate of editing
            target: 'editing.error',
            actions: ['onError'],
          },
        },
      },
      success: {},
    },
  },
  {
    actions: {
      onChange: assign({
        values: (ctx, e) => ({
          ...ctx.values,
          [e.key]: e.value,
        }),
      }),
      onError: assign({
        errors: (_ctx, e) => e.data,
      }),
    },
  },
)
```

## Success State

We did it! A succesful submission. According to the designs there's only one thing left to do here:

- Add another form submission.

Easy peasy, we just transition back to the initial form!

```javascript
const formMachine = Machine(
  {
    initial: 'editing',
    context: {
      values: {},
      errors: {},
    },
    states: {
      editing: {
        initial: 'pristine',
        on: {
          CHANGE: {
            actions: ['onChange'],
          },
          SUBMIT: 'submitting',
        },
        states: {
          pristine: {
            // This is up to you, but I felt like the form needed to be cleared before receiving a new submission
            entry: ['clearForm'],
          },
          error: {},
        },
      },
      submitting: {
        invoke: {
          src: 'onSubmit',
          onDone: 'success',
          onError: {
            target: 'editing.error',
            actions: ['onError'],
          },
        },
      },
      success: {
        on: {
          AGAIN: 'editing',
        },
      },
    },
  },
  {
    actions: {
      onChange: assign({
        values: (ctx, e) => ({
          ...ctx.values,
          [e.key]: e.value,
        }),
      }),
      clearForm: assign({
        values: {},
        errors: {},
      }),
      onError: assign({
        errors: (_ctx, e) => e.data,
      }),
    },
  },
)
```

And that's it! A basic generic state machine that you could use on "any" form using any validation library or method that you want.

Checkout the interactive visualization [here](https://xstate.js.org/viz/?gist=f0bad5c0d06f1fbead95be431e6dc8b7)

![Form State Machine](https://dev-to-uploads.s3.amazonaws.com/i/eu8al7jjsu2bbbuca8kv.PNG)

For the full machine code and an implementation in `React` using `@xstate/react`, check out [this CodeSandbox](https://codesandbox.io/s/busy-lederberg-3c37e)

UI is implemented using the awesome [Chakra UI](https://chakra-ui.com/)