Strong Confirmation Modal with XState
Tags: xstate, react, typescript, javascript
Published: 2021-08-31 17:14:30

The UI element that I'm calling the _Strong Confirmation Modal_ is a prompt to the user to doubly confirm a destructive action. I'll quickly discuss the idea behind this and then show how I implemented it with XState.

## Strong Confirmation

> "Yes, you clicked the _delete_ button, but are you sure? Type the name of the thing you want to delete so that we can both be sure."

I've seen this UI element in plenty of places, but the one that stands out to me is GitHub's. Deleting a repository is definitely a _destructive_ action and not one you'd like to do accidentally. Certainly not something you'd like your cat to be able to trigger by stepping on the keyboard. Here is what it looks like.

![GitHub's confirmation modal for deleting a repository](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ygp886kl3rpizg5nw3pn.png)

You have to type the name of the repository you want to delete in order to enable the button that confirms the deletion. It seems like a small thing, but a UI element like this can go a long way in helping users avoid huge headaches.

## An XState Implementation

Here is the codesandbox if you want to dive right in. Below I'll talk about some of the XState concepts and features that stand out to me.

{% codesandbox tjy24 initialpath=/src/confirmationMachine.ts %}

### Nested States

State machines are hierarchical. They can be made up of [simple and composite states](https://github.com/jbranchaud/til/blob/master/xstate/simple-states-and-composite-states.md). Composite states have sub-states nested within them. (_See the [stately viz](https://stately.ai/viz/8863cb5d-8ec0-4e45-93f0-efd3d4f45607)_)

![XState visualizer showing nested states](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/58nei137zin75lxts0if.png)

Nested states are a great way to represent those concepts that feel like they need multiple booleans. This machine has an `open` state which is composite. While the modal is open, the machine can be in different sub-states. If the input doesn't match the confirmation text, then it stays in `{open: "idle"}`. Once the input matches the confirmation text, the machine will transition to `{open: "confirmable"}`.

### Validations

The piece of this machine that I found trickiest to implement was validation of the input. If the input matches some criteria, I want to move to some _valid_ state. If the input doesn't match, then I need to stay in or move to the _invalid_ state.

I achieved this with an invoked service.

```javascript
{
  services: {
    checkInputConfirmText: (context) => (send) => {
      console.log("Checking input confirm text: ", context.inputConfirmText);

      if (context.doubleConfirmText === context.inputConfirmText) {
        send("REPORT_MATCHING");
      }
    }
  }
}
```

An invoked service can send an event to the machine that invoked it which seemed like the perfect way to trigger the transition I needed. I also took advantage of the fact that an invoked service that exits cleanly will trigger an `onDone` action.

Each time this service is invoked, it will check the validation (do the two text strings match?) and then do one of two things.

1. The validation doesn't pass, it exits, and the `onDone` internally self-transitions back to the `idle` state.

2. The validation does pass, the `REPORT_MATCHING` event is sent, and the invoking machine transitions to the `confirmable` sub-state.

### External Self-Transitions

The `CHANGE` event that is sent each time the modal's input value _changes_ triggers an external self-transition to the `idle` state.

```javascript
open: {
  exit: ["clearErrorMessage"],
  initial: "idle",
  on: {
    CANCEL: {
      target: "#closed"
    },
    CHANGE: {
      target: ".idle",
      internal: false,
      actions: "assignValueToContext"
    }
  },
  states: {
    idle: { /* ... */ },
    confirmable: { /* ... */ }
  }
}
```

A transition of `{ target: ".idle" }` would be an internal transition. That would prevent the validation service from being re-invoked. But I want that service to be invoked on each change, so I include `internal: false` in there to make it an _external_ transition.

## Conclusion

There are lots of other interesting bits going on in this machine beyond what I highlighted. It is worth taking some time to read through it and see what stands out.

Implementing a machine like this was fun because it had a real-world use and I learned a lot while figuring it out. I learned new things about XState and I was pushed to think differently about how to model the problem as a state machine.

_If you enjoy my writing, consider [joining my newsletter](https://crafty-builder-6996.ck.page/e169c61186) or following me on [twitter](https://twitter.com/jbrancha)._