Building a Multi step form with XState and React Hook Form
Tags: typescript, webdev, tutorial, react
Published: 2024-06-09 16:55:58


## What will we be building

Recently I needed a multi step form for one of my personal projects https://www.cvforge.app/, in this app users can create resumes. Starting from scratch with a new resume can be quite overwhelming, to help with this I offer users the option to start from an example. 

This is what that looks like, when a user creates a new resume they are prompted with this dialog:


![CV Forge new resume dialog](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/qwbxfe3ih6mn6o6w6rhb.png)

When they choose to start from scratch, the dialog closes and nothing happens. When they choose to use an example the content of the dialog should change to a selection form. Which will look something like this:


![CV Forge example resume dialog](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/4v9jqfj986joiwk01jya.png)

From here they can either choose an example and continue, or go back to the previous dialog by click "Back".

## Getting started

First we install the necessary dependencies, I assume you already have a React project setup so I will skip over that.

```bash
npm install @xstate/react xstate react-hook-form
```

Next we will create our state machine. We have 2 states, an `initial` state which is the first dialog we show and then an example state which is the second dialog we **only** show if a user chooses to use an example. You can choose any values you like here, the same goes for the name of the state machine. I chose `starterMachine` since I use it for the starter dialog.

```typescript
const starterMachine = createMachine({  
  initial: 'initial',  
  states: {  
    initial: {  
      on: {  
        EXAMPLE: 'example',  
      },  
    },  
    example: {  
      on: {  
        BACK: 'initial',  
      },  
    },  
  },  
});
```

Per state we define what actions we allow for. So when the state is set to initial we have defined an `EXAMPLE` event and when this event is triggered we go to the `example` state. Then for the `example` state we have defined a `BACK` event, when this event is triggered we go back to the initial state as shown in the code snippet.

Next we will create a context for our state machine, which will allow us to easily update the state from within any of our components

```typescript
const { Provider: MachineProvider, useSelector, useActorRef } = createActorContext(starterMachine);
```

Now it is time to render this all out in some components. Let's start by creating components for our different states in the form. The initial state and the example state in my case.

```typescript
function InitialState() {
	const { send } = useActorRef();
	
	return (
		<div>
			// ...
			<Button onClick={() => send({ type: 'EXAMPLE' })}>
				Use an example
			</Button>
		</div>
	)
}
```

I have appropriately named my component InitialState to represent the initial state. As you can see I retrieve the `send` method from the `useActorRef` we get from the context, and use this to update the state of our state machine.

Now let's also create a component to represent the example state, this one includes a form since we will be asking the user for some input. I like to use `react-hook-form` for this in combination with `zod`, but this is of course optional.

```typescript
const exampleSchema = z.object({  
  example_name: z.string({ message: 'Please select an example' }),  
});

function ExampleState() {
	const { send } = useActorRef();

	const form = useForm<z.infer<typeof exampleSchema>>({  
		resolver: zodResolver(exampleSchema),  
	});  
  
	function onSubmit(data: z.infer<typeof exampleSchema>) {  
	  console.log(data) // do something with your data
	}

	<form onSubmit={form.handleSubmit(onSubmit)}>
		// ... input fields here
		<Button type="button" onClick={() => send({ type: 'BACK' })}>  
			Back
		</Button>
		<Button type="submit">  
		  Get Started  
		</Button>
    </form>
}
```

Here we again use the send method to update the state, in this case we send a `BACK` event in case the user wants to go back to the initial state.

Next we need a component to render out the correct component based on the state, for this you can use a ternary, but I like to use a switch case. Here we will use the `useSelector` hook we get from the context to read the value of our state

```typescript
function StateRenderer() {  
  const state = useSelector((state) => state.value);  
  
  switch (state) {  
    case 'initial':  
      return <InitialState />;  
    case 'example':  
      return <ExampleState />;  
    default:  
      return null;  
  }  
}
```

And finally we need to wrap all of this in our `MachineProvider` to ensure all components have access to our context. In my case we will be rendering this in a dialog, but this is of course optional. 

```typescript
export function StarterDialog() {  
  return (  
    <Dialog>  
      <DialogContent>  
        <MachineProvider>
          <StateRenderer /> 
        </MachineProvider>  
      </DialogContent>  
    </Dialog>  
  );  
}
```

We now have a simple example of how to create a funnel using a state machine with XState in react. Explore the [XState documentation](https://xstate.js.org/) to see what more you can do with this extensive library.

Thank you so much for reading, I hope this helps. Let me know if you have any questions or feedback!

Checkout https://www.cvforge.app/ to see it in action and follow me on Twitter/X for more content this https://x.com/atodeshi