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:
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:
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.
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.
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
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.
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.
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
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.
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 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
Top comments (0)