DEV Community

Cover image for A Custom Reusable Delete Confirmation Modal Hook in ReactJS.
Rasel Mahmud
Rasel Mahmud

Posted on

A Custom Reusable Delete Confirmation Modal Hook in ReactJS.

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.

a-custom-reusable-delete-confirmation-modal-hook.png

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>
    </>
  );
}
Enter fullscreen mode Exit fullscreen mode

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
  },
});
Enter fullscreen mode Exit fullscreen mode
  • Single Entry Point – You don’t need to manage isOpen or track the item being deleted. Just call requestConfirmation().
  • 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.

video

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>
  );
}
Enter fullscreen mode Exit fullscreen mode

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);
}
Enter fullscreen mode Exit fullscreen mode
  • 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
  )
);
Enter fullscreen mode Exit fullscreen mode
  • 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) and modal are styled with sensible defaults, but you can override them via options.rootClassName and options.modalClassName.

3. Closing the Modal

function handleClose() {
  const root = document.getElementById('delete-confirmation-root');
  if (root) document.body.removeChild(root);
}
Enter fullscreen mode Exit fullscreen mode
  • 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();
}
Enter fullscreen mode Exit fullscreen mode
  • onConfirm gets a close 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,
});
Enter fullscreen mode Exit fullscreen mode
  • The hook expects a configuration object that describes what to show and how to behave.
  • title, message, and actions define the UI.
  • onConfirm and onCancel 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;
Enter fullscreen mode Exit fullscreen mode

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 inside onConfirm 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 and modalClassName 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"
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

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)