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 SHOW_CONFIRM = 'SHOW_CONFIRM';
export const HIDE_CONFIRM = 'HIDE_CONFIRM';
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;
default:
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]}>
{children}
</ConfirmContext.Provider>
);
};
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 = () => {
closeConfirm();
resolveCallback(true);
};
const onCancel = () => {
closeConfirm();
resolveCallback(false);
};
const confirm = text => {
dispatch({
type: SHOW_CONFIRM,
payload: {
text
}
});
return new Promise((res, rej) => {
resolveCallback = res;
});
};
const closeConfirm = () => {
dispatch({
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
andonCancel
– those are the handlers that will be triggered inside ourConfirmDialog
component when the user clicks the buttons.confirmState
– this is the state value from our Context. It is also used only in theConfirmDialog
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 fromonConfirm
andonCancel
handlers. This is my preferred approach. - Use
CustomEvent
API – dispatch event in theonConfirm
andonCancel
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 = confirmState.show ? (
<div className="portal-overlay">
<div className="confirm-dialog">
<p>{confirmState?.text && confirmState.text}</p>
<div className="confirm-dialog__footer">
<div className="btn" onClick={onConfirm}>
Yes
</div>
<div className="btn" onClick={onCancel}>
Cancel
</div>
</div>
</div>
</div>
) : 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";
ReactDOM.render(
<React.StrictMode>
<ConfirmContextProvider>
<App />
<ConfirmDialog/>
</ConfirmContextProvider>
</React.StrictMode>,
document.getElementById("root")
);
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',
sans-serif;
}
.app {
margin-top:10px;
display:flex;
justify-content: center;
flex-direction: column;
}
.app > * {
text-align:center;
}
.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;
margin-top:20px;
}
.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) {
setMessage('Confirmed!')
} else {
setMessage('Declined.')
}
}
return (
<div className="app">
<div>
<button className="btn" onClick={showConfirm}>Show confirm</button>
</div>
<p>
{message}
</p>
</div>
);
}
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!