Mastering Confirmation Dialogs in React and Next.js - A Hook-Based Approach
Pruthvisinh Rajput / November 30, 2024
When building user interfaces, confirmation dialogs play a critical role in ensuring users don’t accidentally perform destructive actions. However, managing these dialogs can become messy without proper structure. In this blog, I’ll walk you through creating a reusable confirmation dialog in React and Next.js using a custom hook—an elegant and scalable solution for modern applications.
The Challenge
Managing confirmation dialogs traditionally involves inline state management and repetitive code, especially when used across multiple components. This approach:
- Clutters components with unnecessary state and logic.
- Lacks reusability, leading to inconsistent designs.
- Makes scaling and maintenance challenging.
The Solution: A Reusable Hook
React’s hooks allow us to encapsulate stateful logic, making it easy to create
reusable, clean, and maintainable solutions. Let’s introduce useConfirm
, a
custom hook that simplifies the process of implementing confirmation dialogs.
The Code
Here’s the complete implementation of the useConfirm
hook and the dialog
component:
import { useState } from 'react'
import { Button, type ButtonProps } from '@/components/ui/button'
import { ResponsiveModal } from '@/components/responsive-modal'
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle
} from '@/components/ui/card'
export const useConfirm = (
title: string,
message: string,
variant: ButtonProps['variant'] = 'primary'
): [() => JSX.Element, () => Promise<unknown>] => {
const [promise, setPromise] = useState<{
resolve: (value: boolean)=> void
} | null>(null)
const confirm = () => {
return new Promise(resolve => {
setPromise({ resolve })
})
}
const handleClose = () => {
setPromise(null)
}
const handleConfirm = () => {
promise?.resolve(true)
handleClose()
}
const handleCancel = () => {
promise?.resolve(false)
handleClose()
}
const ConfirmDialog = () => (
<ResponsiveModal open={!!promise} onOpenChange={handleClose}>
<Card className='h-full w-full border-none shadow-none'>
<CardContent className='pt-8'>
<CardHeader className='p-0'>
<CardTitle>{title}</CardTitle>
<CardDescription>{message}</CardDescription>
</CardHeader>
<div className='flex w-full flex-col items-center justify-end gap-x-2 gap-y-2 pt-4 lg:flex-row'>
<Button
onClick={handleCancel}
variant={'outline'}
className='w-full lg:w-auto'
>
Cancel
</Button>
<Button
onClick={handleConfirm}
variant={variant}
className='w-full lg:w-auto'
>
Confirm
</Button>
</div>
</CardContent>
</Card>
</ResponsiveModal>
)
return [ConfirmDialog, confirm]
}
Why This Approach Works
1. Clean Separation of Concerns
The useConfirm
hook encapsulates both the UI and state logic, keeping them
isolated from the main components. This reduces clutter and ensures that your
components remain focused on their primary responsibilities.
2. Reusable and Customizable
The useConfirm
hook is designed with flexibility in mind. You can:
- Change the title and message to suit different contexts.
- Adjust the button styles using the
variant
prop for various actions (e.g., primary, destructive, or outline).
This allows you to reuse the dialog across different parts of your application without duplicating code.
3. Scalable Design
By abstracting the confirmation dialog into a hook, this approach ensures consistency and maintainability across your application. Even in projects with multiple confirmation dialogs, you can rely on the same logic and styling, making it easy to scale.
Using the Hook
Here’s an example of how to use useConfirm in a component:
const App = () => {
const [ConfirmDialog, confirm] = useConfirm(
'Delete Item',
'Are you sure you want to delete this item?',
'destructive'
)
const handleDelete = async () => {
const isConfirmed = await confirm()
if (isConfirmed) {
console.log('Item deleted')
// Perform your delete operation here
} else {
console.log('Deletion canceled')
}
}
return (
<div>
<Button onClick={handleDelete} variant='destructive'>
Delete Item
</Button>
<ConfirmDialog />
</div>
)
}
Why This Approach Works
Clean Separation of Concerns
The UI and state logic are isolated in the useConfirm
hook, reducing clutter
in your components. This ensures each part of your application focuses on its
primary responsibility.
Reusable and Customizable
The useConfirm
hook is designed for flexibility:
- Easily adapt it for different actions by changing the title and message.
- Customize the dialog’s button styles using the
variant
prop (e.g., primary, destructive, outline).
This reusability simplifies development and promotes consistency across your application.
Scalable Design
This pattern is highly scalable, making it ideal for projects with multiple confirmation dialogs. By centralizing the logic and styling, you can ensure consistency and maintainability as your application grows.
Conclusion
With React hooks, managing confirmation dialogs becomes a breeze. The
useConfirm
hook allows you to create reusable, customizable, and
scalable dialogs without cluttering your components.
If you found this guide helpful, feel free to share it with others! You can also explore the GitHub repository for the complete codebase and more examples.
Happy coding! 🚀
Note: This implementation uses components from the ShadCN component library. Please refer to the ShadCN documentation for more details on the existing components used in this example.