If you've worked with React for a while, you know the common pattern: every time you need a delete confirmation, you reach for a library like react-confirm
or sweetalert2
. And yes, these tools are stable and do the job.
But here's the catch: they come with extra baggage and limitations.
- They add dependencies to your bundle.
- They are often less flexible to customize.
- They may not fit neatly into your design system.
Been there, done that. I recently wrote a reusable delete confirmation hook (useDeleteConfirmation) that I now use in every project I work on.
It keeps things simple, clean, and fully under your control—without overengineering a simple problem.
- It's under 70 lines of code.
- Provides a consistent UX across the app.
- Works with our UI components out of the box.
This hook allows you to easily request a confirmation before executing irreversible operations.
The Problem With Manual Implementation
Look at how we typically handle delete confirmations manually. We need to track two states (modal open/close and item to delete), which becomes verbose and repetitive across multiple components:
import React, { useState } from 'react';
function UserList() {
const [isOpenDeleteModal, seOpenDeleteModal] = useState(false);
const [userToDelete, setUserToDelete] = useState(null);
function handleDeleteUser(user) {
setUserToDelete(user);
seOpenDeleteModal(true);
}
function handleConfirm() {
if (userToDelete) {
console.log('Deleting user:', userToDelete);
seOpenDeleteModal(false);
setUserToDelete(null);
}
}
function handleCancel() {
seOpenDeleteModal(false);
setUserToDelete(null);
}
return (
<>
{isOpenDeleteModal && (
<div className="modal-overlay">
<div className="modal">
<h1>Are you sure?</h1>
<p>Are you sure you want to delete this user? This action cannot be undone</p>
<div>
<button onClick={handleConfirm}>Yes</button>
<button onClick={handleCancel}>No</button>
</div>
</div>
</div>
)}
<div>
{users.map(user => (
<div key={user.id}>
<span>{user.name}</span>
<button onClick={() => handleDeleteUser(user)}>
Delete
</button>
</div>
))}
</div>
</>
);
}
The Problem With Manual Implementation
At first glance, handling delete confirmations manually seems straightforward. But in practice, it introduces several issues:
- State Explosion : Every component needs to manage modal visibility and track which item is being deleted.
- Code Duplication : Open/close logic and modal markup get repeated across multiple components.
- Tight Coupling : Components handle both business logic (e.g., deleting a user) and UI logic (modal state), making them harder to test and maintain.
- Scalability Problems : Multiple delete scenarios (users, posts, comments, etc.) lead to repetitive modal logic everywhere.
- Inconsistent UX : Manually built modals often vary in wording, styling, or accessibility, breaking design consistency.
- Maintenance Overhead : Small changes (button styles, loading states, spacing) must be updated in many places.
The Solution: useDeleteConfirmation
Hook
Instead of scattering modal state logic across every component, we can centralize everything in a single, reusable hook.
With useDeleteConfirmation
, requesting a confirmation becomes as simple as calling one method—no extra state, no repetitive boilerplate.
Here’s the core API in action:
deleteConfirmation.requestConfirmation({
title: "\"Are you sure?\","
message: (
<>This action is permanent and cannot be undone.</>
),
actions: (onConfirm, onCancel) => (
<>
<button onClick={onConfirm}>Yes</button>
<button onClick={onCancel}>No</button>
</>
),
onConfirm: (close) => {
// Your delete logic (e.g., API call)
close(); // Always call close() to dismiss the modal
},
});
-
Single Entry Point – You don’t need to manage
isOpen
or track the item being deleted. Just callrequestConfirmation()
. - Fully Customizable – You decide how the modal looks (buttons, layout, text). The hook only takes care of the lifecycle.
- Clean API – All options live inside a single configuration object, so it’s easy to read and extend later.
-
Built-in Lifecycle Control – The
close()
callback ensures proper cleanup, even for async operations.
This pattern keeps your components lightweight and focused on business logic, while the hook handles the messy details of modal management.
Here is full example usage:
function UserList() {
const deleteConfirmation = useDeleteConfirmation();
function handleDelete(user) {
deleteConfirmation.requestConfirmation({
title: "<h1 className=\"font-semibold text-3xl\">Are you sure?</h1>,"
message: (
<p className="mt-3">
Are you sure you want to delete {user.name}?
This action cannot be undone.
</p>
),
actions: (onConfirm, onCancel) => (
<div className="flex items-center gap-x-2 justify-end mt-4">
<Button theme="primary" onClick={onConfirm}>Yes</Button>
<Button theme="danger" onClick={onCancel}>No</Button>
</div>
),
onConfirm: (close) => {
// Your API call to delete
console.log('Deleting user:', user);
close(); // Close the confirmation dialog
}
});
}
return (
<div>
{users.map(user => (
<div key={user.id}>
<span>{user.name}</span>
<Button
theme="danger"
onClick={() => handleDelete(user)}
>
Delete
</Button>
</div>
))}
</div>
);
}
Design Patterns & Architecture
This hook leverages several key design patterns that provide core React principles for a clean, maintainable solution:
1. Hook Pattern (Custom Hook)
- Encapsulates modal logic and state management
- Provides a clean, reusable interface
- Follows React's compositional patterns
2. Portal Pattern
- Uses React Portals to render modals outside the component tree
- Prevents CSS inheritance issues and z-index conflicts
- Enables proper modal overlay behavior
3. Render Props Pattern
- The
actions
function uses render props to provide maximum flexibility - Allows complete customization of button layout and styling
- Maintains separation of concerns between logic and presentation
4. Callback Pattern
- Uses callback functions (
onConfirm
,onCancel
) for event handling - Provides controlled cleanup through the
close
callback - Enables async operations with proper modal lifecycle management
5. Configuration Object Pattern
- Single configuration object for all modal options
- Makes the API easy to understand and extend
- Supports optional parameters with sensible defaults
Implementation
The useDeleteConfirmation
hook is intentionally lightweight, yet it follows a few important design principles. Let’s break down how it works step by step:
1. Root Element Management
let deleteConfirmationRoot = document.getElementById('delete-confirmation-root');
if (!deleteConfirmationRoot) {
deleteConfirmationRoot = document.createElement('div');
deleteConfirmationRoot.id = 'delete-confirmation-root';
document.body.appendChild(deleteConfirmationRoot);
}
- A dedicated DOM node (
#delete-confirmation-root
) is created only when the modal is requested. - This keeps the DOM clean and avoids unnecessary hidden modals hanging around.
- If the root already exists, it reuses it.
2. Opening and Rendering the Modal
createRoot(deleteConfirmationRoot).render(
ReactDOM.createPortal(
<div className="overlay...">
<div className="modal...">
...
</div>
</div>,
deleteConfirmationRoot
)
);
- The hook uses React Portals to render the modal outside of the normal component tree.
- This ensures it always appears above everything else and avoids CSS/z-index issues.
- The
overlay
(dark background) andmodal
are styled with sensible defaults, but you can override them viaoptions.rootClassName
andoptions.modalClassName
.
3. Closing the Modal
function handleClose() {
const root = document.getElementById('delete-confirmation-root');
if (root) document.body.removeChild(root);
}
- When the user confirms or cancels, the modal root is completely removed from the DOM.
- This ensures proper cleanup and prevents memory leaks.
4. Confirm and Cancel Handlers
function handleConfirm(onConfirm) {
onConfirm(handleClose);
}
function handleCancel(onCancel) {
onCancel?.();
handleClose();
}
-
onConfirm
gets aclose
callback injected. This allows async logic (like API calls) before closing. -
onCancel
is optional, but if provided, it’s called before cleanup.
5. Configuration Object
deleteConfirmation.requestConfirmation({
title,
message,
actions,
onConfirm,
onCancel,
});
- The hook expects a configuration object that describes what to show and how to behave.
-
title
,message
, andactions
define the UI. -
onConfirm
andonCancel
define the behavior. - If the config is invalid, the hook throws an error immediately to avoid silent failures.
Why This Design Works
-
Stateless by design → no extra
useState
clutter inside your components. - Self-cleaning → creates DOM nodes only when needed, then removes them.
- Composable → you fully control how the modal looks and behaves.
In short: this implementation gives you all the power of a modal without the overhead of managing modal state in every component.
Here's the complete implementation of the useDeleteConfirmation
hook:
import ReactDOM from 'react-dom';
import { createRoot } from 'react-dom/client';
function useDeleteConfirmation(options = {}) {
function handleClose() {
const deleteConfirmationRoot = document.getElementById('delete-confirmation-root');
if (deleteConfirmationRoot) {
document.body.removeChild(deleteConfirmationRoot);
}
}
function handleConfirm(onConfirm) {
onConfirm(handleClose);
}
function handleCancel(onCancel) {
onCancel?.();
handleClose();
}
function requestConfirmation(item) {
if (!item || typeof item.onConfirm !== 'function' || typeof item.actions !== 'function') {
throw new Error('Invalid configuration for useDeleteConfirmation hook.');
}
let deleteConfirmationRoot = document.getElementById('delete-confirmation-root');
if (!deleteConfirmationRoot) {
deleteConfirmationRoot = document.createElement('div');
deleteConfirmationRoot.id = 'delete-confirmation-root';
document.body.appendChild(deleteConfirmationRoot);
}
createRoot(deleteConfirmationRoot).render(
ReactDOM.createPortal(
<div className={`fixed inset-0 bg-black/50 flex items-center justify-center z-50 ${options.rootClassName || ''}`}>
<div className={`bg-white p-6 rounded-md shadow-md max-w-md w-full ${options.modalClassName || ''}`}>
<div>{item.title}</div>
<div className="mt-2">{item.message}</div>
{item.actions(
() => handleConfirm(item.onConfirm),
() => handleCancel(item.onCancel)
)}
</div>
</div>,
deleteConfirmationRoot
)
);
}
return { requestConfirmation };
}
export default useDeleteConfirmation;
Best Practices
- Always warn the user clearly before destructive actions.
- Use destructive button styling (e.g., red for "Delete") to reduce mistakes.
- Always call the provided
close()
callback insideonConfirm
to properly clean up the modal. - Keep confirmation text short, clear, and direct.
- Consider adding loading states during async operations within
onConfirm
. - Use consistent styling across your application by leveraging the
rootClassName
andmodalClassName
options.
Technical Benefits
Performance
- Lazy Loading: Modal DOM elements are only created when needed
- Efficient Cleanup: Automatic cleanup prevents memory leaks
- Minimal Bundle Size: Under 70 lines with no external dependencies
Maintainability
- Single Responsibility: Each function has a clear, focused purpose
- Extensible Design: Easy to add new features without breaking existing code
- Type Safety: Full TypeScript support with proper type definitions
User Experience
- Consistent Behavior: Same interaction patterns across your application
- Accessible: Works with keyboard navigation and screen readers
- Flexible Styling: Integrates seamlessly with your design system
Next.js (SSR) Usage
This hook directly accesses the browser DOM, so it must only run on the client side in Next.js applications.
How to Use in Next.js
Mark your component as client-only by adding at the very top:
"use client"
Alternative SSR-Safe Pattern
For better SSR compatibility, use this pattern:
import { useEffect, useState } from 'react';
function MyComponent() {
const [isClient, setIsClient] = useState(false);
const deleteConfirmation = useDeleteConfirmation();
useEffect(() => {
setIsClient(true);
}, []);
if (!isClient) return null; // or loading state
// Rest of your component
}
Conclusion
With useDeleteConfirmation
, you get the flexibility of a custom modal without the overhead of an external library. It's a simple, reusable solution that keeps your codebase clean and consistent while providing a better user experience for destructive operations.
The hook is lightweight, customizable, and follows React best practices, making it an ideal solution for projects of any size. Give it a try in your next project and say goodbye to bulky confirmation libraries!
Read more from my site:
https://rsraselmahmuddev.vercel.app/articles/a-custom-reusable-delete-confirmation-modal-hook
Top comments (0)