Custom confirm dialog with React Hooks and the Context

The confirm dialogs are used in almost every application. When working on internal apps, the developers tend to get lazy and use the browser’s ugly built-in confirm dialog. The fact that you’re reading this article suggests that you’re looking to replace it with something more sophisticated. Let’s create a custom confirm dialog, using React Hooks, useReducer, and the Context API. Note that there are easier ways to implement it, but in this way, we ensure that we can use our ConfirmDialog across the whole application without the need to import the component explicitly. We just need the hook and all the magic will happen behind.

Before we dive, here is a sneek peek into the end result that we want to achieve:

const { confirm } = useConfirm();
const isConfirmed = await confirm('Do you confirm this action?');

if(isConfirmed) {
    // do something if the user has confirmed
} else {
    // do something else if the user declined

As you see, we’ve wrapped our custom confirm in a Promise which resolves to a boolean value. This is a bit tricky to implement but you will how I’ve handled this below..

Create the Context and the Reducer

First, let’s create a fresh React application:

npx create-react-app custom-confirm-window 

For state management, we will use the amazing combination between the Context API + useReducer:

import React from 'react';

const ConfirmContext = React.createContext();

export default ConfirmContext;

export const initialState = {
    show: false,
    text: ''

export const reducer = (state = initialState, action) => {
    switch (action.type) {
        case SHOW_CONFIRM:
            return {
                show: true,
                text: action.payload.text
        case HIDE_CONFIRM:
            return initialState;
            return initialState;

In Reducer.js, we define the two actions that we need – SHOW_CONFIRM and HIDE_CONFIRM . Our state objects consists of two properties – show(Boolean) and text(String). The text is the message that is displayed in the Confirm dialog. That’s all we need. Let’s now create the custom hook for invoking our window.

Now, let’s also create a new component for our Context Provider wrapper, which we will later use in our index.js to wrap our whole app:

import {useReducer} from "react";
import {initialState, reducer} from "./store/Reducer";
import ConfirmContext from "./store/ConfirmContext";

export const ConfirmContextProvider = ({ children }) => {
    const [state, dispatch] = useReducer(reducer, initialState);

    return (
        <ConfirmContext.Provider value={[state, dispatch]}>

Custom useConfirm hook

This is where things get a bit more complex. You will find an explination below.

import { useContext } from 'react';
import ConfirmContext from '../store/ConfirmContext';
import {HIDE_CONFIRM, SHOW_CONFIRM} from "../store/Reducer";

let resolveCallback;
function useConfirm() {
    const [confirmState, dispatch] = useContext(ConfirmContext);
    const onConfirm = () => {

    const onCancel = () => {
    const confirm = text => {
            type: SHOW_CONFIRM,
            payload: {
        return new Promise((res, rej) => {
            resolveCallback = res;

    const closeConfirm = () => {
            type: HIDE_CONFIRM

    return { confirm, onConfirm, onCancel, confirmState };

export default useConfirm;

Quick explination about the values which our custom hook exports:

  • confirm – this will be the most widely used function from our hook. It returns a Promise which resolves to to a boolean value, which indicates the user’s choice.
  • onConfirm and onCancel – those are the handlers that will be triggered inside our ConfirmDialog component when the user clicks the buttons.
  • confirmState – this is the state value from our Context. It is also used only in the ConfirmDialog component.

I believe that the most complex part here is the Promise in the confirm function. How do we detect if the user has clicked on “Yes” or “Cancel” and resolves this through a Promise? There are 2 main approaches:

  • Assign the reference of the Promise.resolve() callback to another function, which is accessible from onConfirm and onCancel handlers. This is my preferred approach.
  • Use CustomEvent API – dispatch event in the onConfirm and onCancel handlers and resolve the Promise when the event is triggered. This approach is slightly more complex, so I prefer to go with the first one.

To put it simply, inside the onConfirm and onCancel, we resolve the Promise, by calling a reference function pointing to the Promise.resolve(). That’s all the magic.

The ConfirmDialog component

Now that the custom hook is created, let’s focus on the Confirm dialog itself.
First, we need to create a new HTML element in our public/index.html, which will be used for our Portal to mount our ConfirmDialog. It’s generally a good practice to mount dialogs outside the root React element and use a Portal element instead. We will add our element below the already existing root element.

Let’s create the ConfirmDialog now.

<div id="root"></div>
<div id="portal"></div>
import React from 'react';
import { createPortal } from 'react-dom';
import useConfirm from '../hooks/useConfirm';

const ConfirmDialog = () => {
    const { onConfirm, onCancel, confirmState } = useConfirm();

    const portalElement = document.getElementById('portal');
    const component = ? (
        <div className="portal-overlay">
            <div className="confirm-dialog">
                <p>{confirmState?.text && confirmState.text}</p>
                <div className="confirm-dialog__footer">
                    <div className="btn" onClick={onConfirm}>
                    <div className="btn" onClick={onCancel}>
    ) : null;

    return createPortal(component, portalElement);
export default ConfirmDialog;

Later we will add global styles to our App.js. Notice this our component function returns the createPortal a function called with 2 arguments – the component itself and the HTML element where it should be mounted, which is the portal element that we’ve added to public/index.html.

Putting the pieces together

Now it’s time to apply what we’ve built so far. First, let’s edit our index.js by wrapping our App in the our ConfirmContextProvider content.

import React from "react";
import ReactDOM from "react-dom";
import App from "./App";
import {ConfirmContextProvider} from "./store/ConfirmContextProvider";
import ConfirmDialog from "./components/ConfirmDialog";

            <App />

Let’s first create a src/App.css where we will put all the styling needed for our application:

body {
  margin: 0;
  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
    'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',

.app {
  justify-content: center;
  flex-direction: column;

.app > * {

.portal-overlay {
  position: fixed;
  top: 0;
  left: 0;
  bottom: 0;
  right: 0;
  z-index: 1000000;
  width: 100%;
  height: 100%;
  background: rgba(0, 0, 0, 0.5);

.portal-overlay .confirm-dialog {
  z-index: 1000000000000111;
  padding: 16px;
  background-color: white;
  width: 400px;
  position: absolute;
  top: 200px;
  left: 50%;
  transform: translate(-50%, -50%);
  border: 1px solid rgba(0, 0, 0, 0.2);
  border-radius: 5px;

.portal-overlay .confirm-dialog__footer {
  display: flex;
  justify-content: flex-end;

.btn {
  outline: none;
  padding:6px 10px;
  border:1px solid #000;
  margin: 0 10px;

…and our main component App.js:

import React, {useState} from "react";
import useConfirm from "./hooks/useConfirm";
import './App.css';

function App() {
    const {confirm} = useConfirm();
    const [message, setMessage] = useState('');
    const showConfirm = async () => {
        const isConfirmed = await confirm('Do you confirm your choice?');

        if (isConfirmed) {
        } else {
    return (
        <div className="app">
                <button className="btn" onClick={showConfirm}>Show confirm</button>

export default App;

Simple as that! We get this simple and flexible API at the cost of some boilerplate code. There are certainly easier ways to implement it but I prefer this one, as it’s much easier to scale and further customize. This dialog can be very easily transformed into an Alert dialog as well. Enjoy!