# useDelete (https://v2.s3.dimah.dev/docs/react/hooks/delete)

`useDelete` deletes objects through your server. Each key has its own state,
so a list can show which rows are being deleted. A second call for a key that
is already being deleted joins that call.

```tsx title="components/delete-file.tsx"
"use client";

import { useDelete } from "@dimah-s3/react";

export function DeleteFile({ objectKey }: { objectKey: string }) {
  const { remove, get } = useDelete({ route: "attachments" });
  const operation = get(objectKey);

  return (
    <button
      type="button"
      onClick={() => void remove(objectKey)}
      disabled={operation?.phase === "deleting"}
    >
      {operation?.phase === "deleting" ? "Deleting…" : "Delete"}
    </button>
  );
}
```

The hook has no confirm state. To ask first, use the ui
[`Delete`](https://v2.s3.dimah.dev/docs/react/ui/components/delete) root, which owns one confirm
dialog for every trigger inside it, or open your own dialog and call
`remove` when the user confirms.

---

## Delete one key [#delete-one-key]

`remove(key, { fileName })` resolves with an outcome and does not throw.
`fileName` is stored on the operation for display, such as in a toast.

```ts
const outcome = await remove(objectKey, { fileName: "invoice.pdf" });

if (outcome.status === "success") console.log("Deleted", outcome.data.key);
if (outcome.status === "error") console.log(outcome.error.code);
if (outcome.status === "blocked") console.log("beforeDelete said no");
```

A delete has no cancel, so it never ends `aborted`.

## Delete many keys [#delete-many-keys]

`removeMany(items)` takes any number of keys. The hook sends them in batches
of up to 100 (`DELETE_MANY_MAX_KEYS`), one request after another, and every
operation of the call shares one `batchId`. The server deletes each key on its
own, so some can fail while others succeed:

```ts
const { deleted, failed, blocked } = await removeMany([
  "attachments/a.pdf",
  { key: "attachments/b.pdf", fileName: "b.pdf" },
]);

for (const { key, error } of failed) console.log(key, error.code);
```

Keys that are already being deleted are joined rather than sent again, and
their outcomes are folded into the result.

---

## State [#state]

- **`operations`** — every delete still kept, in the order it started. Each has `key`, `fileName`, `batchId`, `phase` (`deleting`, `success`, or `error`), and `error`.
- **`get(key)`** — the newest operation for a key.
- **`pendingKeys`** — keys with a running delete.
- **`isPending`** — `true` while any key is being deleted.
- **`reset(key?)`** — removes settled operations for one key, or every settled operation. It does not cancel a running delete.

## Callbacks [#callbacks]

| Callback               | Runs                                                                                 |
| ---------------------- | ------------------------------------------------------------------------------------ |
| `beforeDelete(keys)`   | Once per call. Return `false` to block the keys. A throw fails them (`HOOK_FAILED`). |
| `onDeleteStart(keys)`  | Once per call, before the request.                                                   |
| `onError(error, key)`  | Once per failed key.                                                                 |
| `onSuccess(data, key)` | Once per deleted key, after every `onError` of the call.                             |
| `onSettled(result)`    | Once per call, last.                                                                 |

`data` is `{ key, bucket? }`. `bucket` is set by `remove` only, because batch
responses do not carry it. A call that joins a running delete does not run the
callbacks again.

---

## Type reference [#type-reference]

```ts
import type {
  DeleteOperation,
  DeleteResult,
  UseDeleteOptions,
  UseDeleteReturn,
} from "@dimah-s3/react";
```

### UseDeleteOptions [#usedeleteoptions]

<AutoTypeTable
  path="packages/react/src/types/delete.ts"
  name="UseDeleteOptions"
/>

### UseDeleteReturn [#usedeletereturn]

<AutoTypeTable
  path="packages/react/src/types/delete.ts"
  name="UseDeleteReturn"
/>

### DeleteOperation [#deleteoperation]

<AutoTypeTable
  path="packages/react/src/types/delete.ts"
  name="DeleteOperation"
/>

### DeleteResult [#deleteresult]

<AutoTypeTable path="packages/react/src/types/delete.ts" name="DeleteResult" />

## Frequently asked questions [#frequently-asked-questions]

<Accordions>

<Accordion title="How do I make sure only the owner can delete a file?">

Check ownership on the server in `delete.guard`. Client checks are not an
authorization boundary. The route's `keyPrefix` only keeps keys inside the
route's folder; it does not know who owns a file. See
[Security](https://v2.s3.dimah.dev/docs/server/security).

```ts title="lib/s3.ts"
import { route, s3Error } from "@dimah-s3/server";

export const attachments = route({
  delete: {
    guard: async ({ key, request }) => {
      const session = await getSession(request);
      if (!session) throw s3Error("UNAUTHORIZED");
      if (!(await isOwner(session.userId, key))) throw s3Error("FORBIDDEN");
    },
  },
});
```

</Accordion>

<Accordion title="How do I clear an error after a failed delete?">

Call `reset(key)`. Render the error from `get(key)?.error`:

```tsx
const { get, reset } = useDelete({ route: "attachments" });
const error = get(objectKey)?.error;

if (error) {
  return (
    <button type="button" onClick={() => reset(objectKey)}>
      {error.message} — dismiss
    </button>
  );
}
```

</Accordion>

</Accordions>
