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:
Dialog
- The main container componentDialogTitle
- The title section of the dialogDialogContent
- The main content areaDialogContentText
- Text content within the dialogDialogActions
- Container for action buttons (typically at the bottom)
These components work together to create a structured, accessible modal experience.
Essential Dialog Props
Prop | Type | Default | Description |
---|---|---|---|
open | boolean | false | Controls whether the dialog is displayed |
onClose | function | - | Callback fired when the dialog is requested to be closed |
fullWidth | boolean | false | If true, the dialog stretches to the maximum width |
maxWidth | 'xs' | 'sm' | 'md' | 'lg' | 'xl' | false | 'sm' | Determines the maximum width of the dialog |
fullScreen | boolean | false | If true, the dialog will be full-screen |
disableEscapeKeyDown | boolean | false | If true, hitting escape will not close the dialog |
disableBackdropClick | boolean | false | If true, clicking the backdrop will not close the dialog |
PaperProps | object | - | Props applied to the Paper element |
TransitionComponent | component | Fade | The component used for the transition |
TransitionProps | object | - | 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:
- Proper focus management - When opened, focus moves to the dialog and is trapped within it
- ARIA attributes - Appropriate roles and labels for screen readers
- Keyboard navigation - Escape key closes the dialog, tab key navigates through focusable elements
- 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:
- Reduced re-renders compared to other form libraries
- Simple validation setup with built-in validators
- Easy integration with MUI form components
- 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:
- Using React state (
useState
) to control the dialog's visibility - Providing handlers for opening, closing, and performing the delete action
- Using MUI's Dialog components to structure the confirmation interface
- 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:
- We're using React Hook Form's
useForm
hook to manage form state and validation - The dialog's Paper component is now a form with an onSubmit handler
- We've added a TextField with validation that requires the user to type "DELETE"
- The Delete button is disabled until the form is valid
- 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:
- Props for customizing all text elements and behavior
- Loading state during the delete operation with a CircularProgress indicator
- Disabling form controls during deletion to prevent multiple submissions
- PropTypes for better documentation and type safety
- 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:
- How to integrate the dialog into a real-world component
- Managing loading state during async operations
- Customizing the dialog text based on the specific item being deleted
- 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:
- Responsive design with fullScreen for mobile devices
- Custom transition animation
- Optional password verification
- Optional reason collection
- Improved UI with dividers and proper spacing
- Close button in the title bar
- Checkbox for explicit acknowledgment
- Customizable trigger button with props forwarding
- 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:
- Tab should move focus between interactive elements
- Escape should close the dialog
- Enter should submit the form when focused on an input
Focus Management
Proper focus management is essential:
- Focus should move to the first focusable element when the dialog opens
- Focus should be trapped within the dialog while it's open
- 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:
aria-labelledby
should point to the dialog titlearia-describedby
should point to the dialog description- 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
-
Always use controlled dialogs - Manage the dialog's open state with React state for predictable behavior.
-
Provide clear feedback - Users should understand exactly what will be deleted and the consequences.
-
Handle loading states - Disable form controls during deletion to prevent multiple submissions.
-
Implement proper error handling - Provide clear error messages if the delete operation fails.
-
Use appropriate validation - The level of confirmation should match the severity of the action.
-
Reset form state - Always reset the form when the dialog closes to prevent stale data.
-
Consider mobile users - Test the dialog on small screens and ensure it's usable on touch devices.
-
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.