# useUpload (https://v2.s3.dimah.dev/docs/react/hooks/upload)

`useUpload` takes files from a picker, a drop, a paste, or your own code. It
checks them against the route's policy, then presigns, uploads, and confirms
each one. The server builds the object key; the hook only sends the route
name.

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

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

export function FileUploader() {
  const upload = useUpload({
    route: "avatar",
    onSuccess: (result) => {
      console.log("Uploaded:", result.key);
    },
  });

  return (
    <div
      {...upload.getRootProps({
        className: "cursor-pointer rounded-lg border border-dashed p-6",
      })}
    >
      <input {...upload.getInputProps()} />
      {upload.isPending
        ? `Uploading: ${upload.progress.percent}%`
        : "Click or drop a file here"}
    </div>
  );
}
```

For ready-made controls, pass the return value to the
[upload components](https://v2.s3.dimah.dev/docs/react/ui/components/upload).

---

## Taking files in [#taking-files-in]

- **`getRootProps(props?, call?)`** — spread on a dropzone. It handles drag and drop, paste, click, and Enter or Space to open the picker.
- **`getInputProps(props?)`** — spread on a hidden `<input type="file">`.
- **`open(call?)`** — opens the file picker from your own button.
- **`handleFiles(files, call?)`** — uploads a `File`, a `File[]`, or a `FileList` from your own code, such as a camera capture.
- **`getDropTargetProps(props?, call?)`** — drag and drop handlers only. It adds no `tabIndex`, no click or keyboard handler, and no `role`, so the element keeps its own interactions. Use it for drop targets such as folder rows.

```tsx
const upload = useUpload({ route: "avatar", noClick: true });

return (
  <div {...upload.getRootProps()}>
    <input {...upload.getInputProps()} />
    <button type="button" onClick={() => upload.open()}>
      Choose file
    </button>
  </div>
);
```

`isDragActive`, `isDragAccept`, and `isDragReject` describe the files dragged
over a dropzone or drop target. `disabled`, `noDrag`, `noClick`, and
`noKeyboard` turn intake off. With `disabled`, `open()` does nothing too, and
the return's `disabled` field is `true` so your pickers can render disabled.
`handleFiles` still uploads the files you pass it.

---

## The queue [#the-queue]

Every file of a call joins one queue. `maxFiles` caps the files per call
(default `1`), and `concurrentFiles` uploads that many at once (default `2`).

```tsx
const upload = useUpload({
  route: "attachments",
  maxFiles: 10,
  concurrentFiles: 3,
  queue: true,
});
```

By default a new call cancels the files still in flight and replaces the
queue once its own files start uploading. Earlier calls that are still
preparing are cancelled at once. A call that is blocked, rejected, or fails
before uploading leaves the queue alone. With `queue: true`, new files are
added to the end and the files in flight keep going.

Each entry in `upload.files` has:

- **`id`** — pass it to `cancel(id)`.
- **`phase`** — `queued`, `preparing`, `presigning`, `uploading`, `finalizing`, `success`, or `error`.
- **`progress`** — `{ loaded, total, percent, speed? }`.
- **`error`** — an `APIError`, set when `phase` is `error`.
- **`result`** — the verified object, set when `phase` is `success`.
- **`fileName`**, **`fileSize`**, **`contentType`**, **`previewUrl`** — for display. `previewUrl` is an object URL for images; the hook revokes it.

```tsx
<ul>
  {upload.files.map((file) => (
    <li key={file.id}>
      {file.fileName} — {file.phase} ({file.progress.percent}%)
      {file.phase !== "success" && file.phase !== "error" ? (
        <button type="button" onClick={() => upload.cancel(file.id)}>
          Cancel
        </button>
      ) : null}
    </li>
  ))}
</ul>
```

The hook also sums up the queue:

- **`phase`** — `idle`, `uploading`, `success`, or `error`.
- **`progress`** — bytes across every file in the queue.
- **`error`** — the failed file's error, or `PARTIAL_FAILURE` when several failed.
- **`isPending`** — `true` while a call is preparing or a file is in flight.
- **`file`** — the first file, or `null`. Handy when `maxFiles` is `1`.

`cancel()` with no id cancels every file. `reset()` removes settled files and
rejections; files in flight keep going.

`retry(id)` uploads a failed file again, and `retry()` every failed file. A
retried file keeps its place and `id`, and goes back to `queued`. It goes with
the call options it was first sent with, so it lands in the same folder: it
is validated again, and `beforeUpload` and `onUploadStart` run again. A retry
never replaces the queue, even without `queue: true`. It resolves like
`handleFiles`, and the callbacks run as for a new call.

```tsx
{
  upload.files.map((file) =>
    file.phase === "error" ? (
      <button key={file.id} type="button" onClick={() => upload.retry(file.id)}>
        Retry {file.fileName}
      </button>
    ) : null,
  );
}
```

---

## Results [#results]

`handleFiles` does not throw for upload errors. It resolves once every file of
the call has ended:

```ts
const { uploaded, failed, blocked, aborted } = await upload.handleFiles(files);

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

`blocked` holds the files `beforeUpload` stopped, and `aborted` the files that
were cancelled or detached. Files rejected before upload are in
`upload.fileRejections`, not in the result.

`result` is the object the server verified with `HeadObject`: `key`,
`bucket`, `size`, `name`, `type`, `metadata`, and the other fields of
`UploadResult`.

---

## Callbacks [#callbacks]

Callbacks run per call or per file:

| Callback                         | Runs                                                                         |
| -------------------------------- | ---------------------------------------------------------------------------- |
| `onFileReject(rejections)`       | Once per call with rejected files, before anything else.                     |
| `beforeUpload(files)`            | Once per call. Return `false` to block it. A throw fails it (`HOOK_FAILED`). |
| `onUploadStart(files)`           | Once per call, after `beforeUpload` allows it.                               |
| `onProgress(progress, file)`     | As bytes are sent.                                                           |
| `onMultipartInit(session, file)` | When a multipart upload starts or resumes.                                   |
| `onPartUpload(part, file)`       | After each multipart part.                                                   |
| `onError(error, file)`           | Once per failed file, after every file of the call has ended.                |
| `onSuccess(result, file)`        | Once per uploaded file, after every `onError` of the call.                   |
| `onSettled(result)`              | Once per call, last. It also runs when every file was rejected.              |

`onSuccess`, `onSettled`, and the other after-hooks are awaited. If one
throws, the error goes to the global `reportError` and the result does not
change.

---

## Request options [#request-options]

`uploadOptions` sets the request for each file: `metadata` for the server
`object` callback, and `fileName` or `contentType` to override what the
browser reports. Pass an object, or a function that gets the file.

```ts
const upload = useUpload({
  route: "attachments",
  uploadOptions: (file) => ({
    metadata: { originalName: file.name },
  }),
});
```

A single call can add its own options. Call fields win, and `metadata`
merges key by key:

```ts
await upload.handleFiles(files, {
  uploadOptions: { metadata: { folder: "invoices" } },
  maxFiles: 3, // lowers maxFiles for this call
});
```

`open`, `getRootProps`, and `getDropTargetProps` take the same call options.
The two prop getters also take a function, so the options are read when the
files arrive:

```tsx
<li
  {...upload.getDropTargetProps({ className: "folder-row" }, () => ({
    uploadOptions: { metadata: { folder: folder.id } },
  }))}
>
  {folder.name}
</li>
```

A drop on a target inside a dropzone uses the target's call options.

`metadata` reaches the server as `clientMetadata`. It is written to S3 only
when the route's `object` callback copies it.

---

## Route policy [#route-policy]

Each call loads the route catalog (`GET /routes`) before it presigns. The
catalog is cached per `S3Api`, so every hook shares one request.

`upload.policy` is the route's policy narrowed by the hook options:

- **`accept`** and **`maxFileSize`** — the stricter of the route and the hook wins.
- **`multipart`** — on when the route allows it and the hook does not pass `multipart: false`.
- **`checksum`** — follows the route. When it is on, single-shot uploads send a SHA-256 of the file.
- **`maxFiles`** — the hook's `maxFiles`.
- **`status`** and **`error`** — where the catalog is in loading.

Files that break the policy are rejected without stopping the others. Each
rejection in `upload.fileRejections` carries `APIError`s with catalog codes:
`FILE_TYPE_NOT_ALLOWED`, `FILE_EMPTY`, `FILE_TOO_LARGE`, or `TOO_MANY_FILES`.
Show them with [`useFormatDimahError`](https://v2.s3.dimah.dev/docs/react/helpers#errors).

```tsx
import { useFormatDimahError, type UseUploadReturn } from "@dimah-s3/react";

export function Rejections({ upload }: { upload: UseUploadReturn }) {
  const formatError = useFormatDimahError();

  return upload.fileRejections.map(({ file, errors }) => (
    <p key={file.name} role="alert">
      {file.name}: {errors.map(formatError).join(", ")}
    </p>
  ));
}
```

To read a route's policy without uploading, for example to label a custom
picker, use `useRouteUploadPolicy`:

```ts
import { useRouteUploadPolicy } from "@dimah-s3/react";

const policy = useRouteUploadPolicy({ route: "avatar" });
// policy.accept, policy.maxFileSize, policy.status
```

---

## Multipart, retries, and unmount [#multipart-retries-and-unmount]

Files of at least 50 MiB (`DEFAULT_MULTIPART_THRESHOLD`) use multipart when
the policy allows it. The server picks the part size and count. Parts upload
`concurrentParts` at a time (default `4`), and each part is hashed as it
streams when the server asks for checksums.

Failed requests retry with exponential backoff. Set `retry: { maxRetries,
baseDelay }` (defaults `3` and `1000` ms). Network errors, S3 errors with
status 429 or 500 and above, and expired part URLs are retried. An expired URL
is signed again first. A file that still fails can be sent again with
`upload.retry(id)`; with an `uploadStore`, a multipart upload that failed on a
retryable error resumes from the parts S3 already has.

- **`cancel(id?)`** aborts the upload, aborts the multipart session on the server, and deletes its stored record.
- **`detach()`** stops every file but keeps the multipart session and its stored record, so the upload can [resume](https://v2.s3.dimah.dev/docs/react/upload-store).
- **Unmounting** detaches files whose call had an `uploadStore` and cancels the rest.

---

## Type reference [#type-reference]

```ts
import type {
  UploadBatchResult,
  UploadCallOptions,
  UploadFileState,
  UploadPolicy,
  UploadRequestOptions,
  UseUploadOptions,
  UseUploadReturn,
} from "@dimah-s3/react";
```

### UseUploadOptions [#useuploadoptions]

<AutoTypeTable
  path="packages/react/src/types/upload.ts"
  name="UseUploadOptions"
/>

### UseUploadReturn [#useuploadreturn]

<AutoTypeTable
  path="packages/react/src/hooks/use-upload.ts"
  name="UseUploadReturn"
/>

### UploadFileState [#uploadfilestate]

<AutoTypeTable
  path="packages/react/src/types/upload.ts"
  name="UploadFileState"
/>

### UploadBatchResult [#uploadbatchresult]

<AutoTypeTable
  path="packages/react/src/types/upload.ts"
  name="UploadBatchResult"
/>

### UploadCallOptions [#uploadcalloptions]

<AutoTypeTable
  path="packages/react/src/types/upload.ts"
  name="UploadCallOptions"
/>

### UploadRequestOptions [#uploadrequestoptions]

<AutoTypeTable
  path="packages/react/src/types/upload.ts"
  name="UploadRequestOptions"
/>

### UploadPolicy [#uploadpolicy]

<AutoTypeTable
  path="packages/react/src/hooks/use-upload.ts"
  name="UploadPolicy"
/>

### UseRouteUploadPolicyOptions [#userouteuploadpolicyoptions]

<AutoTypeTable
  path="packages/react/src/hooks/use-route-upload-policy.ts"
  name="UseRouteUploadPolicyOptions"
/>

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

<Accordions>

<Accordion title="What happens if the route catalog fails to load?">

Every file of that call fails with the catalog's error, and `policy.status` is
`"error"`. The next call loads the catalog again. A custom backend must
implement `catalog()`; see [Bring Your Own Backend](https://v2.s3.dimah.dev/docs/react/custom-backend).

</Accordion>

<Accordion title="How do I show a local image preview before uploading?">

Read `previewUrl` from an entry in `upload.files`. `upload.file?.previewUrl`
is the single-file shorthand:

```tsx
export function AvatarWithPreview() {
  const upload = useUpload({ route: "avatar" });
  const preview = upload.file?.previewUrl;

  return (
    <div {...upload.getRootProps()}>
      <input {...upload.getInputProps()} />
      {preview ? (
        <img
          src={preview}
          alt="Selected avatar"
          className="size-20 rounded-full object-cover"
        />
      ) : (
        "Select avatar"
      )}
    </div>
  );
}
```

</Accordion>

<Accordion title="Why did my earlier files stop when I picked new ones?">

Without `queue: true`, a new call replaces the queue and cancels the files in
flight. Set `queue: true` to append instead.

</Accordion>

<Accordion title="Can I pause and resume an upload?">

Call `detach()` to stop without aborting, then select the same file again to
resume. Resuming needs an `uploadStore` and a multipart upload; see
[Upload Store](https://v2.s3.dimah.dev/docs/react/upload-store).

</Accordion>

</Accordions>
