useDelete
Headless React hook for single, batch, and confirmation-driven object deletion.
useDelete coordinates object deletion with your server API. It supports
two-step confirmation, immediate deletion, and batch operations.
"use client";
import { useDelete } from "@dimah-s3/react";
export function DeleteAvatar({ avatarKey }: { avatarKey: string }) {
const {
isConfirming,
requestDelete,
confirmDelete,
cancelDelete,
isDeleting,
} = useDelete({
route: "avatar",
onSuccess: (key) => {
console.log("Deleted avatar key:", key);
},
});
if (isConfirming) {
return (
<div className="flex items-center gap-2">
<span className="text-sm">Delete avatar?</span>
<button
type="button"
onClick={() => void confirmDelete()}
disabled={isDeleting}
className="text-destructive font-medium"
>
{isDeleting ? "Deleting…" : "Confirm"}
</button>
<button type="button" onClick={cancelDelete}>
Cancel
</button>
</div>
);
}
return (
<button type="button" onClick={() => requestDelete(avatarKey)}>
Delete Avatar
</button>
);
}Deletion workflows
Two-step confirmation flow
Use requestDelete(key) to enter the confirming state for that specific key. This keeps the pending key in state until the user approves (confirmDelete()) or cancels (cancelDelete()).
const { isConfirming, requestDelete, confirmDelete, cancelDelete } = useDelete({
route: "avatar",
});Immediate deletion (remove)
If your UI manages its own modal dialog or requires direct execution without internal confirmation state, call remove(key):
const { remove, isDeleting } = useDelete({ route: "avatar" });
await remove(avatarKey);Batch deletion (removeMany)
Delete multiple keys in one dimah-s3 API request. The server authorizes and deletes each object independently, so the result can contain partial failures:
const { removeMany } = useDelete({ route: "avatar" });
await removeMany([key1, key2, key3]);Type reference
import type { UseDeleteOptions, UseDeleteReturn } from "@dimah-s3/react";UseDeleteOptions
Prop
Type
UseDeleteReturn
Prop
Type
Frequently asked questions
Enforce ownership on the server in delete.guard; client-side checks are not
an authorization boundary. The default keyPrefix only keeps keys inside this
route's folder — it does not know which user owns a file. See
Security.
avatar: route({
delete: {
guard: async ({ key, request }) => {
const session = await getSession(request);
if (!session) throw errors.unauthorized();
const isOwner = await checkOwnership(session.userId, key);
if (!isOwner) throw errors.forbidden();
},
onDeleted: async ({ key, request }) => {
// Database record cleanup
},
},
});Call reset() to return the hook back to the idle phase:
const { reset, error } = useDelete({ route: "avatar" });
if (error) {
return (
<div>
<p className="text-destructive">{error.message}</p>
<button type="button" onClick={reset}>
Try Again
</button>
</div>
);
}