This article is a part of the series "React bits"
How to create an imperative useConfirmationModal hook
Published:
Ever find your self writing code like this?
export function MyComponent() {
const [modalIsOpen, setModalIsOpen] = useState(false);
const [formData, setFormData] = useState<null | FormData>(null);
return <>
<form
onSubmit={(e) => {
e.preventDefault();
setModalIsOpen(true);
setFormData(new FormData(e.currentTarget));
}}
>
<button onClick ={() => setModalIsOpen(true)} type="submit">Submit</form>
</form>
</>
<Modal isOpen={modalIsOpen} onClose={setModalIsOpen(false)}>
<button onClick={() => {
setFormData(null);
setModalIsOpen(false);
}}>Cancel</button>
<button onClick={() => {
setModalIsOpen(false);
// Do whatever with the form data
}}>Confirm</button>
</Modal>
</>
}
This is kind of gross right? We end up including a bunch of logic up in our component for displaying a simple confirmation modal, that messys up and distracts from what we're actually trying to do.
This is where a pattern of encapsulating the open/closed state in a hook, and exposing just the needed parts to the calling components.
Our form with confirmation required can now look like this:
export function MyComponent() {
const [confirmationModal, showConfirmation ] = useConfirmationModal({
title: "Are you sure?",
body: "Some kind of custom text here",
cancelButton: "I am the customised cancel button",
confirmButton: "I am the confirm button",
});
return <>
{confirmationModal}
<form
onSubmit={(e) => {
e.preventDefault();
// Calling the `showConfirmation` function will display the modal.
// If the modal is confirmed, then we trigger the callback passed in.
showConfirmation(() => {
const formData = (e.currentTarget);
//do whatever with the formData;
})
}}
>
<button onClick ={() => setModalIsOpen(true)} type="submit">Submit</form>
</form>
</>
}
This is nice right?
Here's how we can implement such a hook:
1"use client";
2
3import { ReactNode, useRef } from "react";
4
5
6// Avoid instantiating objects as default values for your properties
7// This can lead to infinite loops
8// Instead - instantiate a single object outside of the function
9// See: https://stackoverflow.com/questions/71060000/what-is-the-status-of-the-default-props-rerender-trap-in-react
10const emptyObject = {};
11
12export function useConfirmationModal(options: {
13 title?: ReactNode;
14 body?: ReactNode;
15
16 /* The idea here is that we want to allow the user to provide their own component if they want
17 But we need to make it a renderProp, not just a ReactNode, as we need a way to pass in the click handler*/
18 cancelButton?: string | ((onClick: () => void) => ReactNode);
19 confirmButton?: string | ((onClick: () => void) => ReactNode);
20} = emptyObject): [content: ReactNode, openModal: (onConfirmHandler: () => void) => void] {
21
22 // Setting up our default values
23 const { title = "Are you sure?", body = null, cancelButton = "Cancel", confirmButton = "Confirm" } = options;
24
25 // We store the onConfirm callback in a ref, for use when the user presses the confirm button
26 const onConfirmCallbackRef = useRef(null as null | (() => void));
27
28 // For this example, I'm using the dialog element
29 // You could implement a similar solution using whatever design system you are using.
30 const dialogRef = useRef<HTMLDialogElement>(null);
31
32 const onCancel = () => {
33 // For the dialog element, we interact with the element directly
34 dialogRef.current?.close();
35 }
36
37 const onConfirm = () => {
38 dialogRef.current?.close();
39
40 // Call our stored onConfirm callback
41 onConfirmCallbackRef.current?.();
42 }
43
44 // Declare the JSX to be rendered
45 const content = <dialog ref={dialogRef}>
46 <div>{title}</div>
47 <div>{body}</div>
48
49 {typeof cancelButton === "function" ? cancelButton(onCancel) : <button onClick={onCancel}>
50 {cancelButton}</button>}
51 {typeof confirmButton === "function" ? confirmButton(onConfirm) : <button onClick={onConfirm}>
52 {confirmButton}</button>}
53 </dialog>;
54
55 // Function that will be called to open the modal
56 const showModalFn = (onConfirmHandler: () => void) => {
57 // Show the modal
58 onConfirmCallbackRef.current = onConfirmHandler;
59 // Store the callback to be called if the user clicks confirm
60 dialogRef.current?.showModal();
61 }
62
63
64 return [content, showModalFn]
65}
Find this helpful? Disagree with this approach? Let me know in the comments!
Questions? Comments? Criticisms? Get in the comments! 👇
Spotted an error? Edit this page with Github