I am building the RSVP form for my wedding website and I want allow guests to look themselves up based on their street number.
Happy Path
On the wedding site, the happy path is something like this:
- Ask for the Street Number
- Perform the
lookupGuest
API call - When a guest is found by their street number, display the RSVP form
- Guest fills out and submits the RSVP form
- POST to the
submitRsvp
endpoint - Display a thank you message
Things seems pretty easy! I should be able to knock it out in an evening. But wait....
Complexities
- What if we don't find a guest by street number?
- If a guest has already submitted the RSVP, then they:
- should see how they previously responded.
- shouldn't be able to submit again.
- Street number isn't guaranteed to be unique because we sent multiple invitations to the same address.
- What if any of those API calls fail?
State Machines to the rescue!
In this walkthrough, I'll solve those complexities and more with an XState machine.
DavidKPiano has single handedly put state machines on the map in the front end community (I don't think he gets enough credit for it). Every time I consume his content I think, "Whoa! why isn't everyone doing this?!"
However, in practice I've reached for them a few times, and it always goes like this:
- It takes me a while to remember how to shift my thinking (I get set in my imperative ways). Then it takes me a little bit to look up the syntax.
- Once I do though, I LOVE it! It is so clean and maintainable.
- But then, I go off onto another project that isn't using them and forget everything again.
State machines and XState don't have to be complicated monsters that require a CompSci PHD to wrangle. If you learn just the easiest 10%, you can solve 90% of your problems.
I'm writing this post to help cement my state machine habits, and to serve as a quick reference.
Define your states
First think through all the different states your UI could be in. For the RSVP scenario I'll have:
-
unknown
- This is where I'll ask the guest to look themselves up by street number -
finding
- This will show a loading indicator while waiting for the/lookupGuest
api call -
choosing
- This is where I'll show the guest a list of guests who match the entered street number. -
checkingRsvp
- This is a "transient" state. It's a router. Once a guest is chosen, it'll instantly check to see if that guest has already rsvp'd and route toresponded
orunresponded
-
unresponded
- This will show the RSVP form -
responded
- This will show a readonly view of how the guest RSVPd. This is the last andfinal
step.
Here is how you'd represent that with XState
const rsvpMachine = Machine({
id: 'rsvp',
initial: 'unknown',
context: { },
states: {
unknown: {},
finding: {},
choosing: {},
checkingRsvp: {},
unresponded: {},
submitting: {},
responded: {
type: "final"
},
}
});
If you want to follow along, try pasting this state chart into the XState Visualizer. I built the entire thing this way first, then copy/pasted it into my project.
Define the context
What data needs to stick around between states?
In my case, it will be the guest lookup results
, and the chosen guest
. I'll set them both to null
to start. In an upcoming step, the state machine will pass the context to functions like checkHasResponded
to decide which state to transition to.
const checkHasResponded = (context) => context.guest && context.guest.rsvp;
const checkHasNotResponded = (context) => context.guest && !context.guest.rsvp;
const checkAlreadyChosen = (context) => context.guest;
const rsvpMachine = Machine({
id: 'rsvp',
initial: 'unknown',
context: {
results: null,
guest: null,
},
...
});
Define the user driven events
For each state, what activities can the the user perform?
For example, you can FIND
when in the unknown
state, but you CAN'T FIND
when in the submitting
state.
- When in the
unknown
state, a guest canFIND
themselves by street number, and it should send them to thefinding
state - When in the
choosing
state, a guest canCHOOSE
which lookup result is them, and it should send them to thecheckingRsvp
state. - Entering the
checkingRsvp
should automatically route to theresponded
orunresponded
state. - When in the
unresponded
state a guest canSUBMIT
their RSVP, transitioning them to thesubmitting
state
There are 2 noticeable gaps in the state chart:
- How do you get from
finding
tochoosing
? - How do you get from
submitting
toresponded
? - Both of these are tied to API calls instead of an explicit user interaction.
- I'll cover this in the next step.
Here is the full state machine so far. The events described above are setup with the on
property.
The interesting one is checkingRsvp
. There the event key is blank, which means it will automatically fire. Then, the blank event key is passed multiple targets, each with a condition so it can route accordingly. XState calls this a transient transition.
const checkHasResponded = (context) => context.guest && context.guest.rsvp;
const checkHasNotResponded = (context) => context.guest && !context.guest.rsvp;
const checkAlreadyChosen = (context) => context.guest;
const rsvpMachine = Machine({
id: "rsvp",
initial: "unknown",
context: {
results: null,
guest: null,
},
states: {
unknown: {
on: {
FIND: "finding",
},
},
finding: {},
choosing: {
on: {
CHOOSE: "checkingRsvp",
},
},
checkingRsvp: {
on: {
"": [
{
target: "unresponded",
cond: checkHasNotResponded,
},
{
target: "responded",
cond: checkHasResponded,
},
],
},
},
unresponded: {
on: {
SUBMIT: "submitting",
},
},
submitting: {},
responded: {
type: "final",
},
},
});
Invoking services
The last big piece is figuring out how to make an API call when entering the finding
or the submitting
state. This is done via XState's invoke
property.
To setup an invoke
for for the finding
state:
- Use
invoke.src
to call an async function,lookupGuest
- Setup
onDone.target
to transition to next state when the async call completes - Setup
onDone.actions
toassign
the async result (found inevent.data
) onto thecontext
- XState handles taking the result of the async function and putting it onto
event.data
- XState handles taking the result of the async function and putting it onto
const rsvpMachine = Machine({
...
states: {
...
finding: {
invoke: {
id: "lookupGuest",
// Call the async fn
src: (context, event) => lookupGuest(event.lookupId),
onDone: {
// once the async call is complete
// move to the 'choosing' state
target: 'choosing',
// use xstate's assign action to update the context
actions: assign({
// store the results in context
results: (_, event) => event.data,
// if there was only one result, set the guest
guest: (_, event) => event.data.length === 1 ? event.data[0] : null
})
}
},
},
...
},
});
After implementing the same kind of thing for the submitting
state I was done with the RSVP state machine!
Use it in the UI
You can take a state machine like this and use XState with your framework of choice (vanilla, React, Angular, Vue etc...).
Here's an example of what a React usage might feel like. You can see the current state with state.value
and you can interact with the state machine by using send
to trigger state transition events.
function Rsvp() {
const [state, send] = useMachine(rsvpMachine);
if (state.value === "unknown") {
return (
<GuestLookupForm
onSubmit={(streetNumber) =>
send({ type: "FIND", lookupId: streetNumber })
}
/>
);
}
if (state.value === "finding") {
return <Loading />;
}
if (state.value === "choosing") {
return (
<ChooseGuest
guests={state.context.results}
onSelect={(guest) => send({ type: "CHOOSE", guest})}
/>
);
}
// ...You get the gist
}
Conclusion
It took me an hour or two to build the state chart (all in the visualizer), but once it was done the UI literally just fell into place.
So while it seems like more work up front, it is SOOO worth it! You'd end up working through these complexities regardless. Tackling the logic problems before they are muddied by UI quirks make the solutions so much cleaner and maintainable.
This also just naturally solves problems like "What if I forget to disable the submit button on click, and the user repeatedly mashes on it. Will that submit a bunch of RSVPs?"
With a state machine, the first click would transition to submitting
and after that, the user can send a SUBMIT
action all they want, but submitting
state will just ignore it.
Final Result
Here is the final version of the State Chart, with the additional START_OVER
and onError
capabilities.
This was generated with David's statecharts.io Inspector
Here is a codesandbox demo using the RSVP state machine in React. Take a peek at the source, machine.js
, if you are curious what the final state machine code looks like.
Top comments (4)
There's no trick to state machines, they always add a fair amount of complexity.
In my experience 9/10 times you can solve a problem with a state variable/enum rather than a machine.
I often start writing a state machine, spend way too long on it, have to spend time explaining it to other devs, then realise I've over complicated things and the solution is much cleaner without a machine.
Saying all that, state machines are incredibly powerful when you need fine grained control over state transitioning. It's just I've only found a genuine need for them about 3 times in 8 years 😄
Thanks for the feedback. That is a fair point, and my experience with them very much mirrors yours. I agree with you, it's like there is this magical "line of complexity" you'd need to cross to justify reaching for them.
However, I would argue that line feels deceptively far away due to unfamiliarity (rather than merit). Wide spread familiarity would eliminate a lot of the friction we've both experienced with State Machines (getting up to speed, explaining it, etc...).
To compare and contrast, I think the Redux pattern is just as complex, but we have become VERY familiar with it. Familiar to the point where many (including myself) would contend it is over leveraged. For Redux, the line of complexity feels deceptively close.
All that said, your larger point that State Machines are not a silver bullet is very valid, and maybe I should have had a little blurb on when you would and wouldn't reach for them.
Thanks @davidkpiano for all the awesome work you do for the community. I'm really excited to see what you do with the statecharts.io "Creator". That feels like the thing that could finally tip the scales towards main stream adoption.
RSVP to weddings with XState refers to using the XState library, often utilized in web development for managing state machines, to streamline and automate the RSVP process. This approach can create a robust and responsive RSVP system that handles guest responses, updates guest lists dynamically, and ensures efficient event planning and management. Integrating XState allows for flexibility in handling various RSVP scenarios, enhancing user experience and event organization. wispwillow.com/