Rsvp to weddings with XState
Tags: javascript, webdev, tutorial, react
Published: 2021-02-19 18:51:50

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:

![Happy Path](https://res.cloudinary.com/droopytersen/image/upload/v1613757664/blog/xstate/happy-path.gif)

1. Ask for the Street Number
2. Perform the `lookupGuest` API call
3. When a guest is found by their street number, display the RSVP form
4. Guest fills out and submits the RSVP form
5. POST to the `submitRsvp` endpoint
6. 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](https://xstate.js.org/docs/) machine.

[DavidKPiano](https://twitter.com/DavidKPiano?ref_src=twsrc%5Egoogle%7Ctwcamp%5Eserp%7Ctwgr%5Eauthor) 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:

1. 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.
2. Once I do though, I LOVE it! **It is so clean and maintainable**.
3. 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:

![States](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/u7xyeav8u1ha8o9ioc6l.png)

1. `unknown` - This is where I'll ask the guest to look themselves up by street number
2. `finding` - This will show a loading indicator while waiting for the `/lookupGuest` api call
3. `choosing` - This is where I'll show the guest a list of guests who match the entered street number.
4. `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 to `responded` or `unresponded`
5. `unresponded` - This will show the RSVP form
6. `responded` - This will show a readonly view of how the guest RSVPd. This is the last and `final` step.

*Here is how you'd represent that with XState*
```javascript
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](https://xstate.js.org/viz/). 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.

```javascript
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.

![State Chart with Events](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/eqglseh5g4fgswz8dfsp.png)

1. When in the `unknown` state, a guest can `FIND` themselves by street number, and it should send them to the `finding` state
2. When in the `choosing` state, a guest can `CHOOSE` which lookup result is them, and it should send them to the `checkingRsvp` state.
3. Entering the `checkingRsvp` should automatically route to the `responded` or `unresponded` state.
4. When in the `unresponded` state a guest can `SUBMIT` their RSVP, transitioning them to the `submitting` state

There are 2 noticeable gaps in the state chart:
- How do you get from `finding` to `choosing` ?
- How do you get from `submitting` to `responded`?
- 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](https://xstate.js.org/docs/guides/transitions.html#transient-transitions).

```javascript
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](https://xstate.js.org/docs/guides/communication.html#invoking-services). 

To setup an `invoke` for for the `finding` state:

1. Use `invoke.src` to call an async function, `lookupGuest`
2. Setup `onDone.target` to transition to next state when the async call completes
3. Setup `onDone.actions` to `assign` the async result (found in `event.data` ) onto the `context`
    - XState handles taking the result of the async function and putting it onto `event.data`

```javascript
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.

```jsx
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](https://statecharts.io/)*
![Final State Chart](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/kt786sjamzdt7pwwe5p1.png)

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.
{% codesandbox q5cs8 %}