React.js - Promisified Modals
Or how to create Message Boxes
April 13, 2020 / 3 min read
https://unsplash.com/@erdaest

According to Microsoft:

A message box is a special dialog box that an application can use to display messages and prompt for simple input. A message box typically contains a text message and one or more buttons.

Websites might not see this type of dialog often but web technologies are used more and more to develop desktop applications where these message boxes are a common pattern. Modals in general may even be considered bad UX, but in some situations they might still be the best option. As stated by NNG, modals may be used when:

  • The user is about to take an action that has serious consequences and is difficult to reverse.

  • It’s essential to collect a small amount of information before letting users proceed to the next step in a process.

  • The content in the overlay is urgent, and users are more likely to notice it in an overlay.

Let's consider a to-do list app and assume that removing items from a list is an irreversible operation, and as such, an action with serious consequences. We have a button that when triggered should remove one or more items from the list but before proceeding we want to ask user's confirmation. Putting this into code, we get something like this:

const handleRemove = items => {
  // 1. Ask user's confirmation
  // 2. Permanently remove the items
}

return <button onClick={handleRemove}>Remove item</button>

How can we implement this in React without too much code?

A common pattern for showing/hiding DOM elements in React consists of this:

const [isVisible, setVisible] = React.useState(false)

return (
  <button onClick={() => setVisible(!isVisible)}> Remove items </button>)
  {isVisible && <MessageBox>Are you sure?</MessageBox>}
)

However, following this approach the code would get messy really fast. Why? Because we need to define at least two handlers and to connect them somehow: one to show the MessageBox and another one to actually remove the items. The code would become hard to read because the button that says Remove items is actually not removing the items but showing some other component instead. How that component leads to the actual items removal is also not obvious. On top of that, you probably want to use message boxes to confirm several other actions in the app, so the less the code you need to write, the better. We simply want to get a yes or no from the user, right?

The solution: a promisified modal, so we can do this:

const handleRemove = async items => {
  // 1. Ask user's confirmation
  const result = await MessageBox.open({
    title: "Confirm",
    content: <p>Are you sure?</p>
    buttons: [
      {name: "Oh yeah", handler: () => "yeah"},
      {name: "Nevermind", handler: () => "nope" },
    ]
  })
  // 2. Permanently remove the items
  if(result === "yeah") {
    // ... remove the items
  }
}

So, how do we actually render the MessageBox? By calling ReactDOM.render() which allow us to specify the HTML element where is should be rendered at. Once we are done, we simply call ReactDOM.unmountComponentAtNode() to remove it from the DOM.

Finally, because getting user's input is an asyncronous operation, we wrap the whole thing in a Promise.

// MessageBox.jsx

export default {
  open: props => {
    return new Promise(resolve => {
      const { container, title, content, buttons } = props
      const containerElement = document.querySelector(container)

      const handleClose = value => {
        const result = value
        ReactDOM.unmountComponentAtNode(containerElement)
        return resolve(result)
      }

      const handleButton = handler => () => {
        handleClose(handler())
      }

      ReactDOM.render(
        <Modal title={title} onClose={handleClose}>
          {content}
          {buttons.map(btn => {
            return (
              <button onClick={handleButton(btn.handler)}>{btn.name}</button>
            )
          })}
        </Modal>,
        containerElement
      )
    })
  }
}

And that's it. You can see a fully working example in the CodeSandbox bellow.

Edit React: MessageBox

website written with Vue.js + Nuxt.js and statically hosted at Netlify