Delete with a Custom Confirmation Dialog
Open:
client/src/components/TransactionList.jsx
client/src/components/ConfirmDialog.jsx
The Ledger should not use window.confirm() or confirm(). A browser-native prompt cannot match the application’s design, cannot explain the exact record in a reusable layout, and gives us little control over loading or keyboard behavior.
Create ConfirmDialog.jsx as a reusable component. A minimal version needs these props:
open
title
description
confirmLabel
cancelLabel
busy
onConfirm
onCancel
Render nothing when open is false:
if (!open) return null;
For an open dialog, render an overlay and a dialog box containing the explanation and two explicit actions:
<div
className="fixed inset-0 z-50 flex items-center justify-center p-4"
role="presentation"
>
<div
role="dialog"
aria-modal="true"
aria-labelledby="confirm-title"
>
<h2 id="confirm-title">{title}</h2>
<p>{description}</p>
<button type="button" onClick={onCancel} disabled={busy}>
{cancelLabel}
</button>
<button type="button" onClick={onConfirm} disabled={busy}>
{busy ? "deleting…" : confirmLabel}
</button>
</div>
</div>
The responsibilities are intentionally clear:
opencontrols whether the dialog exists.titleidentifies the action.descriptionexplains the consequence.onCancelcloses the dialog without modifying data.onConfirmstarts the delete operation.busyprevents a second click while the request is running.
In TransactionList, remember which transaction the user wants to delete:
const [confirmDeleteId, setConfirmDeleteId] = useState(null);
const transactionToDelete = monthTx.find(
(transaction) => transaction.id === confirmDeleteId
) ?? null;
The trash button should open the dialog instead of deleting immediately:
<button
type="button"
onClick={() => setConfirmDeleteId(transaction.id)}
aria-label="Delete entry"
>
<Trash2 size={14} />
</button>
Render the dialog with transaction-specific information:
<ConfirmDialog
open={Boolean(transactionToDelete)}
title={`Delete “${transactionToDelete?.description}”?`}
description="This entry will be removed from the ledger."
confirmLabel="Delete entry"
cancelLabel="Keep entry"
onCancel={() => setConfirmDeleteId(null)}
onConfirm={() =>
transactionToDelete && handleDelete(transactionToDelete)
}
/>
The optional chaining operator ?. prevents an error if there is briefly no selected transaction. Boolean(...) converts the transaction object or null into the true/false value expected by open.
The custom dialog created here will be reused for category deletion in Module 5. That is why it belongs in components/ rather than inside TransactionList.jsx.
Test it
Click a transaction’s trash icon. The custom Ledger dialog should open inside the page.
Choose Keep entry and verify that nothing changes. Open it again and choose Delete entry.
There should be no browser-native confirmation prompt.
Checkpoint
Transaction deletion is now an explicit in-app action, the user can read exactly what is being removed, and the confirmation component is reusable for another destructive action.