CodingNic

Transactions

Delete with a Custom Confirmation Dialog

Transactions 24 min read

Delete with a Custom Confirmation Dialog

Open:

text
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:

javascript
open
 title
 description
 confirmLabel
 cancelLabel
 busy
 onConfirm
 onCancel

Render nothing when open is false:

jsx
if (!open) return null;

For an open dialog, render an overlay and a dialog box containing the explanation and two explicit actions:

jsx
<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:

  • open controls whether the dialog exists.
  • title identifies the action.
  • description explains the consequence.
  • onCancel closes the dialog without modifying data.
  • onConfirm starts the delete operation.
  • busy prevents a second click while the request is running.

In TransactionList, remember which transaction the user wants to delete:

javascript
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:

jsx
<button
  type="button"
  onClick={() => setConfirmDeleteId(transaction.id)}
  aria-label="Delete entry"
>
  <Trash2 size={14} />
</button>

Render the dialog with transaction-specific information:

jsx
<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.