Oleksandr Dubenko

Imperative confirm dialog in React

September 07, 2019

Demo + Code

I love the simplicity of the window.confirm function, it provides a concise way to get a yes/no answer from a user.

const answer = window.confirm("Delete item?")
// > answer
// true

You don’t have to manage isOpen state or anything like that.

Most of the time declarative approach fits solving UI problems better. But I think that confirm dialog is not one of them.

While window.confirm gets jobe done, it supports little customization and it’s up to browser to style that dialog.

I wanted to make a similar flow but without compromising on customization ability.

Let’s start by figuring out how the signature of that function would look like.

Lately, I found the approach of writing out types of the functions before writing implementation to be very helpful.

const customConfirm = (DialogContent): boolean => {
  /*...*/
}

I want to pass a react component that will be displayed and as a return value I want to get an answer value.

For this example let’s limit the answer type to boolean true/false.

This looks fine but right now this function signature appears to be synchronous (just like window.confirm) which means that the main thread will be blocked starting from the moment the dialog opens and until the answer is given.

We can improve it by making the function return a Promise.

const customConfirm = (DialogContent): Promise<boolean> => {
  /*...*/
}

Also, we need to have the ability to give the answer (duh) from DialogContent component, so let’s specify that also.

const customConfirm = (DialogContent: (props: {
  giveAnswer: (answer: boolean) => void}): Promise<boolean> => {
  /*...*/
}

We’ll pass giveAnswer function as a prop to that component

And now the implementation:

import * as React from "react"
import * as ReactDOM from "react-dom"

const confirmRoot = document.createElement("div")const body = document.querySelector("body")body.appendChild(confirmRoot)
const customConfirm = (
  DialogContent: (props: {
    giveAnswer: (answer: boolean) => void
  }) => React.ReactElement
): Promise<boolean> =>
  new Promise(res => {
    const giveAnswer = (answer: boolean) => {
      ReactDOM.unmountComponentAtNode(confirmRoot)
      res(answer)
    }

    ReactDOM.render(<DialogContent giveAnswer={giveAnswer} />, confirmRoot)
  })

We start from creating a DOM element confirmRoot where we will render our confirm dialog and appending it to the body.

import * as React from "react"
import * as ReactDOM from "react-dom"

const confirmRoot = document.createElement("div")const body = document.querySelector("body")body.appendChild(confirmRoot)
const customConfirm = (
  DialogContent: (props: {
    giveAnswer: (answer: boolean) => void
  }) => React.ReactElement
): Promise<boolean> =>
  new Promise(res => {
    const giveAnswer = (answer: boolean) => {
      ReactDOM.unmountComponentAtNode(confirmRoot)
      res(answer)
    }

    ReactDOM.render(<DialogContent giveAnswer={giveAnswer} />, confirmRoot)
  })

Next, we define giveAnswer function that will accept answer from DialogContent content, close dialog and then return the given answer.

import * as React from "react"
import * as ReactDOM from "react-dom"

const confirmRoot = document.createElement("div")
const body = document.querySelector("body")
body.appendChild(confirmRoot)

const customConfirm = (
  DialogContent: (props: {
    giveAnswer: (answer: boolean) => void
  }) => React.ReactElement
): Promise<boolean> =>
  new Promise(res => {
    const giveAnswer = (answer: boolean) => {
      ReactDOM.unmountComponentAtNode(confirmRoot)
      res(answer)
    }

    ReactDOM.render(<DialogContent giveAnswer={giveAnswer} />, confirmRoot)  })

And finally, we need to render given dialog to the root created just above.

And now let’s see the usage of this customConfirm function we just wrote

const ConfirmDialog = ({ giveAnswer }) => (
  <div className="ConfirmDialog">
    <button onClick={() => giveAnswer(true)}>Yes</button>
    <button onClick={() => giveAnswer(false)}>No</button>
  </div>
)

function App() {
  const deleteItem = async () => {
    const answer = await customConfirm(ConfirmDialog)    console.log("Your answer is", answer)
  }

  return (
    <div className="App">
      <button onClick={deleteItem}>Delete item</button>
    </div>
  )
}

And now we have a function that has a similar behavior as window.confirm but with great customizability.

The code and demo is available in this sandbox

Thanks for reading :)


Blog by Oleksandr Dubenko