Menu

How to Use React MUI Dialog to Build a Delete Confirmation Flow with React Hook Form

When building React applications, confirmation dialogs are essential UI elements that help prevent users from accidentally performing destructive actions. Material UI's Dialog component provides an elegant solution for these interactions, and when combined with React Hook Form (RHF), you can create robust, validated confirmation flows with minimal effort.

In this comprehensive guide, I'll walk you through creating a delete confirmation dialog using MUI Dialog and React Hook Form. We'll build a practical, production-ready solution that you can adapt to your own projects, covering everything from basic implementation to advanced customizations.

Learning Objectives

By the end of this tutorial, you'll be able to:

  • Implement a fully functional delete confirmation dialog using MUI Dialog
  • Integrate React Hook Form for form validation within dialogs
  • Handle dialog state properly in React applications
  • Apply accessibility best practices to modal dialogs
  • Customize the appearance and behavior of MUI Dialogs
  • Avoid common pitfalls when working with dialogs in React

Understanding the MUI Dialog Component

The Dialog component in Material UI creates a modal window that appears in front of the app content to provide critical information or request user decisions. It's particularly useful for confirmation flows where you need explicit user consent before performing actions like deletion.

Core Dialog Structure

A typical MUI Dialog consists of several specialized components:

  1. Dialog - The main container component
  2. DialogTitle - The title section of the dialog
  3. DialogContent - The main content area
  4. DialogContentText - Text content within the dialog
  5. DialogActions - Container for action buttons (typically at the bottom)

These components work together to create a structured, accessible modal experience.

Essential Dialog Props

PropTypeDefaultDescription
openbooleanfalseControls whether the dialog is displayed
onClosefunction-Callback fired when the dialog is requested to be closed
fullWidthbooleanfalseIf true, the dialog stretches to the maximum width
maxWidth'xs' | 'sm' | 'md' | 'lg' | 'xl' | false'sm'Determines the maximum width of the dialog
fullScreenbooleanfalseIf true, the dialog will be full-screen
disableEscapeKeyDownbooleanfalseIf true, hitting escape will not close the dialog
disableBackdropClickbooleanfalseIf true, clicking the backdrop will not close the dialog
PaperPropsobject-Props applied to the Paper element
TransitionComponentcomponentFadeThe component used for the transition
TransitionPropsobject-Props applied to the Transition element

Controlled vs. Uncontrolled Dialog

Like many React components, MUI Dialog can be used in both controlled and uncontrolled modes:

Controlled Dialog: You manage the dialog's open state through React state, providing both the open prop and an onClose handler to update that state. This is the recommended approach for most applications as it gives you explicit control over when the dialog appears.

Uncontrolled Dialog: While technically possible to create uncontrolled dialogs, it's generally not recommended for confirmation flows where you need precise control over dialog visibility.

Dialog Accessibility Features

MUI Dialog implements several accessibility features by default:

  1. Proper focus management - When opened, focus moves to the dialog and is trapped within it
  2. ARIA attributes - Appropriate roles and labels for screen readers
  3. Keyboard navigation - Escape key closes the dialog, tab key navigates through focusable elements
  4. Focus restoration - When closed, focus returns to the element that opened the dialog

React Hook Form Overview

React Hook Form (RHF) is a performant, flexible form validation library for React that minimizes re-renders and provides a straightforward API. When combined with MUI Dialog, it enables robust form validation within modal interfaces.

Key benefits of using RHF with MUI Dialog include:

  1. Reduced re-renders compared to other form libraries
  2. Simple validation setup with built-in validators
  3. Easy integration with MUI form components
  4. Flexible error handling and display

Building a Delete Confirmation Dialog

Now let's build a practical delete confirmation dialog that incorporates both MUI Dialog and React Hook Form. We'll start with the basic setup and progressively enhance our implementation.

Setting Up the Project

First, let's ensure we have all the necessary dependencies installed:

npm install @mui/material @emotion/react @emotion/styled react-hook-form

If you're using TypeScript (recommended for type safety), also install the types:

npm install --save-dev @types/react @types/react-dom

Creating a Basic Delete Confirmation Dialog

Let's start with a simple delete confirmation dialog that appears when a user clicks a delete button:

import React, { useState } from 'react';
import {
  Button,
  Dialog,
  DialogActions,
  DialogContent,
  DialogContentText,
  DialogTitle,
} from '@mui/material';
import DeleteIcon from '@mui/icons-material/Delete';

const BasicDeleteDialog = () => {
  const [open, setOpen] = useState(false);

  const handleOpen = () => {
    setOpen(true);
  };

  const handleClose = () => {
    setOpen(false);
  };

  const handleDelete = () => {
    // Perform delete operation here
    console.log('Item deleted');
    setOpen(false);
  };

  return (
    <>
      <Button 
        variant="contained" 
        color="error" 
        startIcon={<DeleteIcon />} 
        onClick={handleOpen}
      >
        Delete Item
      </Button>
      
      <Dialog
        open={open}
        onClose={handleClose}
        aria-labelledby="delete-dialog-title"
        aria-describedby="delete-dialog-description"
      >
        <DialogTitle id="delete-dialog-title">
          Confirm Deletion
        </DialogTitle>
        <DialogContent>
          <DialogContentText id="delete-dialog-description">
            Are you sure you want to delete this item? This action cannot be undone.
          </DialogContentText>
        </DialogContent>
        <DialogActions>
          <Button onClick={handleClose}>Cancel</Button>
          <Button onClick={handleDelete} color="error" autoFocus>
            Delete
          </Button>
        </DialogActions>
      </Dialog>
    </>
  );
};

export default BasicDeleteDialog;

This basic implementation demonstrates several key concepts:

  1. Using React state (useState) to control the dialog's visibility
  2. Providing handlers for opening, closing, and performing the delete action
  3. Using MUI's Dialog components to structure the confirmation interface
  4. Adding proper ARIA attributes for accessibility

Integrating React Hook Form

Now, let's enhance our dialog by adding React Hook Form. We'll require users to type "DELETE" to confirm the action, providing an additional safety measure:

import React, { useState } from 'react';
import { useForm } from 'react-hook-form';
import {
  Button,
  Dialog,
  DialogActions,
  DialogContent,
  DialogContentText,
  DialogTitle,
  TextField,
  Box,
  Typography,
} from '@mui/material';
import DeleteIcon from '@mui/icons-material/Delete';

const DeleteConfirmationDialog = () => {
  const [open, setOpen] = useState(false);
  const {
    register,
    handleSubmit,
    formState: { errors, isValid },
    reset,
  } = useForm({
    mode: 'onChange',
    defaultValues: {
      confirmText: '',
    },
  });

  const handleOpen = () => {
    setOpen(true);
  };

  const handleClose = () => {
    setOpen(false);
    reset();
  };

  const onSubmit = (data) => {
    // Perform delete operation here
    console.log('Item deleted', data);
    handleClose();
  };

  return (
    <>
      <Button 
        variant="contained" 
        color="error" 
        startIcon={<DeleteIcon />} 
        onClick={handleOpen}
      >
        Delete Item
      </Button>
      
      <Dialog
        open={open}
        onClose={handleClose}
        aria-labelledby="delete-dialog-title"
        aria-describedby="delete-dialog-description"
        PaperProps={{
          component: 'form',
          onSubmit: handleSubmit(onSubmit),
        }}
      >
        <DialogTitle id="delete-dialog-title">
          Confirm Deletion
        </DialogTitle>
        <DialogContent>
          <DialogContentText id="delete-dialog-description">
            This action cannot be undone. To confirm deletion, please type "DELETE" in the field below.
          </DialogContentText>
          <Box mt={2}>
            <TextField
              fullWidth
              label="Type DELETE to confirm"
              {...register('confirmText', {
                required: 'Confirmation text is required',
                validate: (value) => 
                  value === 'DELETE' || 'Please type DELETE to confirm',
              })}
              error={!!errors.confirmText}
              helperText={errors.confirmText?.message}
              autoFocus
              margin="dense"
            />
          </Box>
        </DialogContent>
        <DialogActions>
          <Button onClick={handleClose}>Cancel</Button>
          <Button 
            type="submit" 
            color="error" 
            disabled={!isValid}
          >
            Delete
          </Button>
        </DialogActions>
      </Dialog>
    </>
  );
};

export default DeleteConfirmationDialog;

This enhanced version introduces several important improvements:

  1. We're using React Hook Form's useForm hook to manage form state and validation
  2. The dialog's Paper component is now a form with an onSubmit handler
  3. We've added a TextField with validation that requires the user to type "DELETE"
  4. The Delete button is disabled until the form is valid
  5. Form state is reset when the dialog closes

Creating a Reusable Delete Confirmation Component

Now let's create a more reusable component that can be used throughout an application:

import React, { useState } from 'react';
import { useForm } from 'react-hook-form';
import PropTypes from 'prop-types';
import {
  Button,
  Dialog,
  DialogActions,
  DialogContent,
  DialogContentText,
  DialogTitle,
  TextField,
  Box,
  CircularProgress,
} from '@mui/material';

const DeleteConfirmationDialog = ({
  title = 'Confirm Deletion',
  description = 'This action cannot be undone.',
  confirmationText = 'DELETE',
  confirmationLabel = `Type ${confirmationText} to confirm`,
  cancelButtonText = 'Cancel',
  deleteButtonText = 'Delete',
  onDelete,
  isDeleting = false,
  itemName = 'this item',
}) => {
  const [open, setOpen] = useState(false);
  const {
    register,
    handleSubmit,
    formState: { errors, isValid },
    reset,
  } = useForm({
    mode: 'onChange',
    defaultValues: {
      confirmText: '',
    },
  });

  const handleOpen = () => {
    setOpen(true);
  };

  const handleClose = () => {
    if (isDeleting) return; // Prevent closing while delete operation is in progress
    setOpen(false);
    reset();
  };

  const onSubmit = async (data) => {
    try {
      await onDelete();
      handleClose();
    } catch (error) {
      // Error handling can be implemented here
      console.error('Delete operation failed:', error);
    }
  };

  return (
    <>
      <Button 
        variant="contained" 
        color="error" 
        onClick={handleOpen}
        size="small"
      >
        Delete
      </Button>
      
      <Dialog
        open={open}
        onClose={handleClose}
        aria-labelledby="delete-dialog-title"
        aria-describedby="delete-dialog-description"
        PaperProps={{
          component: 'form',
          onSubmit: handleSubmit(onSubmit),
        }}
        maxWidth="sm"
        fullWidth
      >
        <DialogTitle id="delete-dialog-title">
          {title}
        </DialogTitle>
        <DialogContent>
          <DialogContentText id="delete-dialog-description">
            Are you sure you want to delete {itemName}? {description}
          </DialogContentText>
          <Box mt={2}>
            <TextField
              fullWidth
              label={confirmationLabel}
              {...register('confirmText', {
                required: 'Confirmation text is required',
                validate: (value) => 
                  value === confirmationText || `Please type ${confirmationText} to confirm`,
              })}
              error={!!errors.confirmText}
              helperText={errors.confirmText?.message}
              autoFocus
              margin="dense"
              disabled={isDeleting}
            />
          </Box>
        </DialogContent>
        <DialogActions>
          <Button 
            onClick={handleClose}
            disabled={isDeleting}
          >
            {cancelButtonText}
          </Button>
          <Button 
            type="submit" 
            color="error" 
            disabled={!isValid || isDeleting}
            startIcon={isDeleting ? <CircularProgress size={20} /> : null}
          >
            {isDeleting ? 'Deleting...' : deleteButtonText}
          </Button>
        </DialogActions>
      </Dialog>
    </>
  );
};

DeleteConfirmationDialog.propTypes = {
  title: PropTypes.string,
  description: PropTypes.string,
  confirmationText: PropTypes.string,
  confirmationLabel: PropTypes.string,
  cancelButtonText: PropTypes.string,
  deleteButtonText: PropTypes.string,
  onDelete: PropTypes.func.isRequired,
  isDeleting: PropTypes.bool,
  itemName: PropTypes.string,
};

export default DeleteConfirmationDialog;

This reusable component adds several important features:

  1. Props for customizing all text elements and behavior
  2. Loading state during the delete operation with a CircularProgress indicator
  3. Disabling form controls during deletion to prevent multiple submissions
  4. PropTypes for better documentation and type safety
  5. Error handling for the delete operation

Using the Reusable Component

Here's how you would use this reusable component in a parent component:

import React, { useState } from 'react';
import { Card, CardContent, Typography, Box } from '@mui/material';
import DeleteConfirmationDialog from './DeleteConfirmationDialog';

const UserCard = ({ user, onDeleteUser }) => {
  const [isDeleting, setIsDeleting] = useState(false);

  const handleDelete = async () => {
    setIsDeleting(true);
    try {
      // Simulate API call with delay
      await new Promise(resolve => setTimeout(resolve, 1500));
      await onDeleteUser(user.id);
    } finally {
      setIsDeleting(false);
    }
  };

  return (
    <Card sx={{ mb: 2 }}>
      <CardContent>
        <Box display="flex" justifyContent="space-between" alignItems="center">
          <div>
            <Typography variant="h6">{user.name}</Typography>
            <Typography variant="body2" color="text.secondary">
              {user.email}
            </Typography>
          </div>
          <DeleteConfirmationDialog
            onDelete={handleDelete}
            isDeleting={isDeleting}
            itemName={`user "${user.name}"`}
            description="All user data will be permanently removed from our servers."
          />
        </Box>
      </CardContent>
    </Card>
  );
};

const UserList = () => {
  const [users, setUsers] = useState([
    { id: 1, name: 'John Doe', email: 'john@example.com' },
    { id: 2, name: 'Jane Smith', email: 'jane@example.com' },
    { id: 3, name: 'Bob Johnson', email: 'bob@example.com' },
  ]);

  const deleteUser = (userId) => {
    setUsers(users.filter(user => user.id !== userId));
  };

  return (
    <Box p={3}>
      <Typography variant="h4" gutterBottom>
        User Management
      </Typography>
      {users.map(user => (
        <UserCard 
          key={user.id} 
          user={user} 
          onDeleteUser={deleteUser} 
        />
      ))}
    </Box>
  );
};

export default UserList;

This implementation shows:

  1. How to integrate the dialog into a real-world component
  2. Managing loading state during async operations
  3. Customizing the dialog text based on the specific item being deleted
  4. Proper error handling and state management

Advanced Dialog Customization

Now that we have a solid foundation, let's explore some advanced customization options for our MUI Dialog.

Styling the Dialog with the sx Prop

MUI's sx prop provides a powerful way to customize components directly:

<Dialog
  open={open}
  onClose={handleClose}
  PaperProps={{
    component: 'form',
    onSubmit: handleSubmit(onSubmit),
    sx: {
      borderRadius: 2,
      boxShadow: 10,
      '& .MuiDialogTitle-root': {
        backgroundColor: 'primary.main',
        color: 'primary.contrastText',
        fontSize: '1.2rem',
      },
      '& .MuiDialogActions-root': {
        padding: 2,
        borderTop: '1px solid',
        borderColor: 'divider',
      },
    },
  }}
>
  {/* Dialog content */}
</Dialog>

Custom Transitions

You can customize the dialog's entrance and exit animations:

import { Slide } from '@mui/material';

// Define the transition
const Transition = React.forwardRef(function Transition(props, ref) {
  return <Slide direction="up" ref={ref} {...props} />;
});

// Use it in the Dialog
<Dialog
  TransitionComponent={Transition}
  TransitionProps={{ timeout: 500 }}
  open={open}
  onClose={handleClose}
>
  {/* Dialog content */}
</Dialog>

Responsive Dialogs

For better mobile experiences, you can make dialogs responsive:

import { useTheme, useMediaQuery } from '@mui/material';

const ResponsiveDialog = () => {
  const theme = useTheme();
  const fullScreen = useMediaQuery(theme.breakpoints.down('md'));
  
  return (
    <Dialog
      fullScreen={fullScreen}
      maxWidth="sm"
      fullWidth
      open={open}
      onClose={handleClose}
    >
      {/* Dialog content */}
    </Dialog>
  );
};

Custom Dialog with Theming

For app-wide dialog styling, you can use MUI's theming system:

import { createTheme, ThemeProvider } from '@mui/material/styles';

const theme = createTheme({
  components: {
    MuiDialog: {
      styleOverrides: {
        paper: {
          borderRadius: 8,
          boxShadow: '0 8px 40px -12px rgba(0,0,0,0.3)',
          padding: 8,
        },
      },
    },
    MuiDialogTitle: {
      styleOverrides: {
        root: {
          fontSize: '1.25rem',
          borderBottom: '1px solid #eee',
          marginBottom: 16,
          padding: '16px 24px',
        },
      },
    },
    MuiDialogActions: {
      styleOverrides: {
        root: {
          borderTop: '1px solid #eee',
          marginTop: 16,
          padding: '16px 24px',
        },
      },
    },
  },
});

// Wrap your app or component with the ThemeProvider
<ThemeProvider theme={theme}>
  <DeleteConfirmationDialog />
</ThemeProvider>

Advanced Form Validation with React Hook Form

Let's explore more advanced validation scenarios with React Hook Form in our dialog.

Custom Validation Logic

You can implement more complex validation rules:

const {
  register,
  handleSubmit,
  formState: { errors, isValid },
  watch,
  reset,
} = useForm({
  mode: 'onChange',
  defaultValues: {
    confirmText: '',
    reason: '',
    understandConsequences: false,
  },
});

// In your JSX:
<TextField
  {...register('confirmText', {
    required: 'Confirmation text is required',
    validate: {
      matchesDeleteText: (value) => 
        value === 'DELETE' || 'Please type DELETE to confirm',
      noSpaces: (value) => 
        !/\s/.test(value) || 'Spaces are not allowed',
    },
  })}
  error={!!errors.confirmText}
  helperText={errors.confirmText?.message}
/>

<TextField
  label="Reason for deletion (optional)"
  multiline
  rows={2}
  fullWidth
  margin="dense"
  {...register('reason', {
    maxLength: {
      value: 200,
      message: 'Reason cannot exceed 200 characters',
    },
  })}
  error={!!errors.reason}
  helperText={errors.reason?.message || `${watch('reason').length}/200`}
/>

<FormControlLabel
  control={
    <Checkbox
      {...register('understandConsequences', {
        required: 'You must acknowledge the consequences',
      })}
    />
  }
  label="I understand this action cannot be undone"
/>
{errors.understandConsequences && (
  <Typography color="error" variant="caption">
    {errors.understandConsequences.message}
  </Typography>
)}

Handling Asynchronous Validation

For some scenarios, you might need to validate against a server:

const {
  register,
  handleSubmit,
  formState: { errors, isValid, isSubmitting },
  setError,
  reset,
} = useForm({
  mode: 'onChange',
  defaultValues: {
    confirmText: '',
    password: '',
  },
});

// Password field with async validation
<TextField
  type="password"
  label="Your password"
  fullWidth
  margin="dense"
  {...register('password', {
    required: 'Password is required',
    validate: async (value) => {
      // Simulate API call to verify password
      try {
        const response = await fetch('/api/verify-password', {
          method: 'POST',
          headers: { 'Content-Type': 'application/json' },
          body: JSON.stringify({ password: value }),
        });
        const data = await response.json();
        
        if (!data.valid) {
          return 'Incorrect password';
        }
        return true;
      } catch (error) {
        return 'Failed to verify password';
      }
    }
  })}
  error={!!errors.password}
  helperText={errors.password?.message}
/>

Complete Delete Confirmation Dialog Implementation

Now, let's put everything together to create a comprehensive, production-ready delete confirmation dialog component:

import React, { useState } from 'react';
import { useForm } from 'react-hook-form';
import PropTypes from 'prop-types';
import {
  Button,
  Dialog,
  DialogActions,
  DialogContent,
  DialogContentText,
  DialogTitle,
  TextField,
  Box,
  CircularProgress,
  FormControlLabel,
  Checkbox,
  Typography,
  useTheme,
  useMediaQuery,
  Slide,
  Divider,
  IconButton,
} from '@mui/material';
import CloseIcon from '@mui/icons-material/Close';
import DeleteIcon from '@mui/icons-material/Delete';

// Slide transition for the dialog
const Transition = React.forwardRef(function Transition(props, ref) {
  return <Slide direction="up" ref={ref} {...props} />;
});

const DeleteConfirmationDialog = ({
  title = 'Confirm Deletion',
  description = 'This action cannot be undone.',
  confirmationText = 'DELETE',
  confirmationLabel = `Type ${confirmationText} to confirm`,
  cancelButtonText = 'Cancel',
  deleteButtonText = 'Delete',
  onDelete,
  isDeleting = false,
  itemName = 'this item',
  requirePassword = false,
  verifyPassword = () => Promise.resolve(true),
  requireReason = false,
  onReasonProvided = () => {},
  showTriggerButton = true,
  triggerButtonProps = {},
}) => {
  const [open, setOpen] = useState(false);
  const theme = useTheme();
  const fullScreen = useMediaQuery(theme.breakpoints.down('sm'));
  
  const {
    register,
    handleSubmit,
    formState: { errors, isValid },
    watch,
    reset,
    setError,
  } = useForm({
    mode: 'onChange',
    defaultValues: {
      confirmText: '',
      password: '',
      reason: '',
      understandConsequences: false,
    },
  });

  const handleOpen = () => {
    setOpen(true);
  };

  const handleClose = () => {
    if (isDeleting) return; // Prevent closing while delete operation is in progress
    setOpen(false);
    reset();
  };

  const onSubmit = async (data) => {
    try {
      // Verify password if required
      if (requirePassword) {
        const isPasswordValid = await verifyPassword(data.password);
        if (!isPasswordValid) {
          setError('password', {
            type: 'manual',
            message: 'Incorrect password',
          });
          return;
        }
      }
      
      // Save reason if provided
      if (requireReason && data.reason) {
        onReasonProvided(data.reason);
      }
      
      // Perform delete operation
      await onDelete();
      handleClose();
    } catch (error) {
      // Error handling
      console.error('Delete operation failed:', error);
    }
  };

  return (
    <>
      {showTriggerButton && (
        <Button 
          variant="contained" 
          color="error" 
          onClick={handleOpen}
          startIcon={<DeleteIcon />}
          size="small"
          {...triggerButtonProps}
        >
          Delete
        </Button>
      )}
      
      <Dialog
        open={open}
        onClose={handleClose}
        aria-labelledby="delete-dialog-title"
        aria-describedby="delete-dialog-description"
        PaperProps={{
          component: 'form',
          onSubmit: handleSubmit(onSubmit),
          sx: {
            borderRadius: 2,
            maxWidth: fullScreen ? '100%' : '500px',
          },
        }}
        maxWidth="sm"
        fullWidth
        fullScreen={fullScreen}
        TransitionComponent={Transition}
      >
        <DialogTitle id="delete-dialog-title" sx={{ pr: 6 }}>
          {title}
          <IconButton
            aria-label="close"
            onClick={handleClose}
            disabled={isDeleting}
            sx={{
              position: 'absolute',
              right: 8,
              top: 8,
              color: (theme) => theme.palette.grey[500],
            }}
          >
            <CloseIcon />
          </IconButton>
        </DialogTitle>
        
        <Divider />
        
        <DialogContent>
          <DialogContentText id="delete-dialog-description">
            Are you sure you want to delete {itemName}? {description}
          </DialogContentText>
          
          <Box mt={3} mb={1}>
            <TextField
              fullWidth
              label={confirmationLabel}
              {...register('confirmText', {
                required: 'Confirmation text is required',
                validate: (value) => 
                  value === confirmationText || `Please type ${confirmationText} to confirm`,
              })}
              error={!!errors.confirmText}
              helperText={errors.confirmText?.message}
              autoFocus
              margin="dense"
              disabled={isDeleting}
            />
          </Box>
          
          {requirePassword && (
            <Box my={1}>
              <TextField
                type="password"
                fullWidth
                label="Your password"
                {...register('password', {
                  required: 'Password is required',
                })}
                error={!!errors.password}
                helperText={errors.password?.message}
                margin="dense"
                disabled={isDeleting}
              />
            </Box>
          )}
          
          {requireReason && (
            <Box my={1}>
              <TextField
                label="Reason for deletion (optional)"
                multiline
                rows={2}
                fullWidth
                margin="dense"
                {...register('reason', {
                  maxLength: {
                    value: 200,
                    message: 'Reason cannot exceed 200 characters',
                  },
                })}
                error={!!errors.reason}
                helperText={errors.reason?.message || `${watch('reason').length}/200`}
                disabled={isDeleting}
              />
            </Box>
          )}
          
          <Box mt={2}>
            <FormControlLabel
              control={
                <Checkbox
                  {...register('understandConsequences', {
                    required: 'You must acknowledge the consequences',
                  })}
                  disabled={isDeleting}
                />
              }
              label="I understand this action cannot be undone"
            />
            {errors.understandConsequences && (
              <Typography color="error" variant="caption" display="block">
                {errors.understandConsequences.message}
              </Typography>
            )}
          </Box>
        </DialogContent>
        
        <Divider />
        
        <DialogActions sx={{ px: 3, py: 2 }}>
          <Button 
            onClick={handleClose}
            disabled={isDeleting}
            variant="outlined"
          >
            {cancelButtonText}
          </Button>
          <Button 
            type="submit" 
            color="error" 
            variant="contained"
            disabled={!isValid || isDeleting}
            startIcon={isDeleting ? <CircularProgress size={20} color="inherit" /> : <DeleteIcon />}
          >
            {isDeleting ? 'Deleting...' : deleteButtonText}
          </Button>
        </DialogActions>
      </Dialog>
    </>
  );
};

DeleteConfirmationDialog.propTypes = {
  title: PropTypes.string,
  description: PropTypes.string,
  confirmationText: PropTypes.string,
  confirmationLabel: PropTypes.string,
  cancelButtonText: PropTypes.string,
  deleteButtonText: PropTypes.string,
  onDelete: PropTypes.func.isRequired,
  isDeleting: PropTypes.bool,
  itemName: PropTypes.string,
  requirePassword: PropTypes.bool,
  verifyPassword: PropTypes.func,
  requireReason: PropTypes.bool,
  onReasonProvided: PropTypes.func,
  showTriggerButton: PropTypes.bool,
  triggerButtonProps: PropTypes.object,
};

export default DeleteConfirmationDialog;

This comprehensive implementation includes:

  1. Responsive design with fullScreen for mobile devices
  2. Custom transition animation
  3. Optional password verification
  4. Optional reason collection
  5. Improved UI with dividers and proper spacing
  6. Close button in the title bar
  7. Checkbox for explicit acknowledgment
  8. Customizable trigger button with props forwarding
  9. Comprehensive error handling and validation

Usage Examples

Here are a few examples of how to use our advanced delete confirmation dialog:

Basic Usage

import DeleteConfirmationDialog from './DeleteConfirmationDialog';

const ProductItem = ({ product, onDelete }) => {
  const [isDeleting, setIsDeleting] = useState(false);

  const handleDelete = async () => {
    setIsDeleting(true);
    try {
      await deleteProduct(product.id);
    } finally {
      setIsDeleting(false);
    }
  };

  return (
    <div>
      <h3>{product.name}</h3>
      <DeleteConfirmationDialog
        onDelete={handleDelete}
        isDeleting={isDeleting}
        itemName={product.name}
      />
    </div>
  );
};

With Password Verification

import DeleteConfirmationDialog from './DeleteConfirmationDialog';

const AdminUserList = () => {
  const [users, setUsers] = useState([/* users data */]);
  const [isDeleting, setIsDeleting] = useState(false);
  const [selectedUser, setSelectedUser] = useState(null);

  const verifyAdminPassword = async (password) => {
    // Make API call to verify admin password
    const response = await fetch('/api/admin/verify-password', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ password }),
    });
    const data = await response.json();
    return data.valid;
  };

  const handleDeleteUser = async () => {
    if (!selectedUser) return;
    
    setIsDeleting(true);
    try {
      await fetch(`/api/users/${selectedUser.id}`, {
        method: 'DELETE',
      });
      setUsers(users.filter(user => user.id !== selectedUser.id));
    } finally {
      setIsDeleting(false);
      setSelectedUser(null);
    }
  };

  return (
    <div>
      {users.map(user => (
        <div key={user.id}>
          <span>{user.name}</span>
          <button onClick={() => setSelectedUser(user)}>Delete</button>
        </div>
      ))}
      
      {selectedUser && (
        <DeleteConfirmationDialog
          open={!!selectedUser}
          onClose={() => setSelectedUser(null)}
          onDelete={handleDeleteUser}
          isDeleting={isDeleting}
          itemName={selectedUser.name}
          requirePassword={true}
          verifyPassword={verifyAdminPassword}
          showTriggerButton={false}
          title="Delete User Account"
          description="This will remove all user data from the system."
        />
      )}
    </div>
  );
};

With Reason Collection

import DeleteConfirmationDialog from './DeleteConfirmationDialog';

const ContentModeration = () => {
  const [posts, setPosts] = useState([/* posts data */]);
  const [isDeleting, setIsDeleting] = useState(false);
  
  const handleDelete = async (postId, reason) => {
    setIsDeleting(true);
    try {
      await fetch(`/api/posts/${postId}`, {
        method: 'DELETE',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ reason }),
      });
      setPosts(posts.filter(post => post.id !== postId));
    } finally {
      setIsDeleting(false);
    }
  };

  const handleReasonProvided = (postId, reason) => {
    console.log(`Reason for deleting post ${postId}: ${reason}`);
  };

  return (
    <div>
      {posts.map(post => (
        <div key={post.id}>
          <h3>{post.title}</h3>
          <p>{post.content}</p>
          <DeleteConfirmationDialog
            onDelete={() => handleDelete(post.id)}
            isDeleting={isDeleting}
            itemName={`post "${post.title}"`}
            requireReason={true}
            onReasonProvided={(reason) => handleReasonProvided(post.id, reason)}
            title="Remove Content"
            description="This content will be removed from the platform."
            triggerButtonProps={{
              variant: 'outlined',
              children: 'Remove Content',
            }}
          />
        </div>
      ))}
    </div>
  );
};

Accessibility Considerations

Accessibility is crucial for modal dialogs. Here are some key considerations:

Keyboard Navigation

Ensure users can navigate the dialog using only the keyboard:

  1. Tab should move focus between interactive elements
  2. Escape should close the dialog
  3. Enter should submit the form when focused on an input

Focus Management

Proper focus management is essential:

  1. Focus should move to the first focusable element when the dialog opens
  2. Focus should be trapped within the dialog while it's open
  3. Focus should return to the triggering element when the dialog closes

ARIA Attributes

MUI Dialog includes important ARIA attributes by default, but ensure you're providing meaningful values:

  1. aria-labelledby should point to the dialog title
  2. aria-describedby should point to the dialog description
  3. Error messages should be properly associated with their inputs

Screen Reader Announcements

For dynamic content changes, consider using live regions:

import { SnackbarContent } from '@mui/material';

// For important announcements during the delete process
const [announcement, setAnnouncement] = useState('');

// In your component
<div 
  role="status" 
  aria-live="polite" 
  className="sr-only"
>
  {announcement}
</div>

// When starting deletion
setAnnouncement('Deletion in progress');

// When completed
setAnnouncement('Item successfully deleted');

Best Practices and Common Issues

Best Practices

  1. Always use controlled dialogs - Manage the dialog's open state with React state for predictable behavior.

  2. Provide clear feedback - Users should understand exactly what will be deleted and the consequences.

  3. Handle loading states - Disable form controls during deletion to prevent multiple submissions.

  4. Implement proper error handling - Provide clear error messages if the delete operation fails.

  5. Use appropriate validation - The level of confirmation should match the severity of the action.

  6. Reset form state - Always reset the form when the dialog closes to prevent stale data.

  7. Consider mobile users - Test the dialog on small screens and ensure it's usable on touch devices.

  8. Maintain focus management - Ensure keyboard focus is properly managed for accessibility.

Common Issues and Solutions

Dialog Closing Unexpectedly

Issue: Dialog closes when clicking inside the dialog but outside form elements.

Solution: Use disableBackdropClick and ensure the dialog doesn't close on inner clicks:

<Dialog
  open={open}
  onClose={handleClose}
  disableBackdropClick
  onClick={(e) => e.stopPropagation()}
>
  {/* Dialog content */}
</Dialog>

Form Submission Issues

Issue: Form submits even when validation fails.

Solution: Ensure you're using React Hook Form's handleSubmit wrapper correctly:

<Dialog
  PaperProps={{
    component: 'form',
    onSubmit: handleSubmit(onSubmit),
  }}
>
  {/* Dialog content */}
</Dialog>

Dialog Re-renders Too Often

Issue: Dialog causes performance issues due to frequent re-renders.

Solution: Memoize callback functions and optimize form validation:

// Memoize callbacks
const handleDelete = useCallback(async () => {
  // Delete logic
}, [dependencies]);

// Use shouldUnregister: false to prevent unnecessary re-renders
const { register, handleSubmit } = useForm({
  shouldUnregister: false,
});

Focus Management Issues

Issue: Focus gets lost or behaves unpredictably when dialog opens/closes.

Solution: Use refs to manage focus explicitly:

const DeleteDialog = () => {
  const [open, setOpen] = useState(false);
  const triggerButtonRef = useRef(null);
  const firstInputRef = useRef(null);
  
  useEffect(() => {
    // When dialog opens, focus the first input
    if (open && firstInputRef.current) {
      firstInputRef.current.focus();
    }
  }, [open]);
  
  const handleClose = () => {
    setOpen(false);
    // When dialog closes, return focus to trigger button
    setTimeout(() => {
      if (triggerButtonRef.current) {
        triggerButtonRef.current.focus();
      }
    }, 0);
  };
  
  return (
    <>
      <Button 
        ref={triggerButtonRef}
        onClick={() => setOpen(true)}
      >
        Delete
      </Button>
      
      <Dialog open={open} onClose={handleClose}>
        <DialogContent>
          <TextField 
            inputRef={firstInputRef}
            // other props
          />
        </DialogContent>
      </Dialog>
    </>
  );
};

Performance Optimization

For applications with many dialogs or complex forms, consider these optimizations:

Lazy Loading

Load the dialog component only when needed:

import React, { lazy, Suspense, useState } from 'react';
import { Button, CircularProgress } from '@mui/material';

// Lazy load the dialog component
const DeleteConfirmationDialog = lazy(() => import('./DeleteConfirmationDialog'));

const ProductItem = ({ product, onDelete }) => {
  const [showDialog, setShowDialog] = useState(false);
  
  return (
    <div>
      <h3>{product.name}</h3>
      <Button 
        color="error" 
        onClick={() => setShowDialog(true)}
      >
        Delete
      </Button>
      
      {showDialog && (
        <Suspense fallback={<CircularProgress />}>
          <DeleteConfirmationDialog
            showTriggerButton={false}
            open={showDialog}
            onClose={() => setShowDialog(false)}
            onDelete={() => onDelete(product.id)}
            itemName={product.name}
          />
        </Suspense>
      )}
    </div>
  );
};

Memoization

Use React.memo and useCallback to prevent unnecessary re-renders:

import React, { memo, useCallback, useState } from 'react';

// Memoize the dialog component
const DeleteConfirmationDialog = memo(({ onDelete, itemName }) => {
  // Component implementation
});

const ProductList = () => {
  const [products, setProducts] = useState([/* products data */]);
  
  // Memoize the delete handler
  const handleDelete = useCallback((productId) => {
    setProducts(products.filter(p => p.id !== productId));
  }, [products]);
  
  return (
    <div>
      {products.map(product => (
        <div key={product.id}>
          <DeleteConfirmationDialog
            onDelete={() => handleDelete(product.id)}
            itemName={product.name}
          />
        </div>
      ))}
    </div>
  );
};

Wrapping Up

In this comprehensive guide, we've built a robust delete confirmation dialog using MUI Dialog and React Hook Form. We've covered everything from basic implementation to advanced customization, accessibility, and performance optimization.

By integrating these two powerful libraries, we've created a reusable component that provides a secure, user-friendly way to confirm destructive actions in your React applications. The combination of MUI's elegant UI components and React Hook Form's efficient validation makes for a smooth, accessible user experience while protecting against accidental data loss.

Remember that confirmation dialogs should be used judiciously - not every action needs confirmation, but for destructive operations like deletion, they're an essential safeguard. By following the patterns and best practices outlined in this guide, you can create confirmation flows that are both secure and user-friendly.