DEV Community

Cover image for Strong Confirmation Modal with XState
Josh Branchaud
Josh Branchaud

Posted on • Updated on <time datetime="2021-09-01T20:34:25Z" class="date-no-year">Sep 1</time>

Strong Confirmation Modal with XState

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

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.

Nested States

State machines are hierarchical. They can be made up of simple and composite states. Composite states have sub-states nested within them. (See the stately viz)

XState visualizer showing nested states

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.

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

      if (context.doubleConfirmText === context.inputConfirmText) {
        send("REPORT_MATCHING");
      }
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

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.

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

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 or following me on twitter.

Discussion (0)