Return Deleted Data for Undo
Open:
server/src/routes/transactions.js
client/src/lib/api.js
client/src/context/BudgetContext.jsx
client/src/components/TransactionList.jsx
The starter DELETE route returns an empty success response. That is enough for permanent deletion, but it leaves the browser with no copy of the deleted record to restore.
Change the route so it returns the transaction that was removed:
router.delete("/:id", async (req, res) => {
const db = await getDb();
const index = db.transactions.findIndex(
(transaction) => transaction.id === req.params.id
);
if (index === -1) {
return res.status(404).json({ error: "Transaction not found" });
}
const [deleted] = db.transactions.splice(index, 1);
await saveDb();
res.json(deleted);
});
splice() removes one array item and returns the removed item. That returned object becomes the payload sent back to the client.
Update the client helper so it uses the normal request() function and receives that JSON object:
export const deleteTransaction = (id) =>
request(`/transactions/${id}`, {
method: "DELETE",
});
In BudgetContext, remove the transaction from shared state and return the server response:
async function removeTransaction(id) {
const deleted = await api.deleteTransaction(id);
setTransactions((previous) =>
previous.filter((transaction) => transaction.id !== id)
);
return deleted;
}
Now the list can remember the deleted object for a short time:
const [deletedTransaction, setDeletedTransaction] = useState(null);
async function handleDelete(transaction) {
const deleted = await removeTransaction(transaction.id);
setDeletedTransaction(deleted);
setConfirmDeleteId(null);
}
Use an effect to clear the undo state after five seconds:
useEffect(() => {
if (!deletedTransaction) return;
const timer = window.setTimeout(
() => setDeletedTransaction(null),
5000
);
return () => window.clearTimeout(timer);
}, [deletedTransaction]);
The cleanup function cancels the old timer when the deleted transaction changes or the component unmounts.
Undo can use the normal create operation:
await addTransaction({
date: deletedTransaction.date,
description: deletedTransaction.description,
category: deletedTransaction.category,
amount: deletedTransaction.amount,
type: deletedTransaction.type,
});
This implementation detail is important: undo is not an undelete endpoint. It creates a new transaction containing the same visible data. Because the normal create endpoint generates a new id, the restored record can have a different id from the original.
Test it
Delete a transaction. A message should explain that the transaction was deleted and can be undone for five seconds.
Click undo before the timer ends. The transaction should return with the same date, description, category, amount, and type.
Delete another transaction and wait longer than five seconds. The undo action should disappear.
Checkpoint
A successful delete returns the removed data, the client can hold that data briefly, and undo restores the entry through the normal create workflow.