# useDownload (https://v2.s3.dimah.dev/docs/react/hooks/download)

Two hooks download objects from a named route:

- **`useDownload`** presigns a GET URL and hands it to the browser, which saves or opens the file. No bytes pass through your page.
- **`useFetchDownload`** presigns, then fetches the bytes in the page with progress and cancel, and saves them.

Both track each key on its own. Several keys can download at once, and a
second call for a key that is already running joins that call.

```tsx title="components/download-report.tsx"
"use client";

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

export function DownloadReport({ objectKey }: { objectKey: string }) {
  const { download, get } = useDownload({ route: "reports" });
  const isPending = get(objectKey)?.phase === "presigning";

  return (
    <button
      type="button"
      onClick={() => void download(objectKey, { fileName: "report.pdf" })}
      disabled={isPending}
    >
      {isPending ? "Preparing…" : "Download"}
    </button>
  );
}
```

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

---

## useDownload [#usedownload]

`download(key, { fileName })` presigns the key, then clicks a temporary link
to the URL. The response's `Content-Disposition` decides whether the browser
saves the file or opens it. `disposition` on the hook asks for `inline` or
`attachment`; the server can override it.

```tsx
const { download } = useDownload({
  route: "reports",
  disposition: "attachment",
  onSuccess: (data, key) => {
    console.log("Browser has the URL for", key, data.url);
  },
});

const outcome = await download(objectKey);
if (outcome.status === "error") console.log(outcome.error.code);
```

`onSuccess` runs once the browser has the URL, not when the file finishes
saving.

### Presign without downloading [#presign-without-downloading]

`presign(key, options?)` returns the signed URL without handing it to the
browser, for example to copy a link. `presignMany(files)` signs several keys,
four at a time. Both run `beforeDownload`, but they record no operation and
run no other callback.

```ts
const outcome = await presign(objectKey, {
  fileName: "report.pdf",
  expiresIn: 900, // seconds; the server caps it at the route's limit
});
if (outcome.status === "success") {
  await navigator.clipboard.writeText(outcome.data.url);
}

const { signed, failed, blocked } = await presignMany([
  { key: firstKey },
  { key: secondKey, expiresIn: 3600 },
]);
```

---

## useFetchDownload [#usefetchdownload]

`useFetchDownload` fetches the bytes in the page and saves them as a file. It
reports progress and can be cancelled.

```tsx title="components/fetch-download.tsx"
"use client";

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

export function FetchDownload({ objectKey }: { objectKey: string }) {
  const { download, cancel, get } = useFetchDownload({ route: "documents" });
  const operation = get(objectKey);
  const running =
    operation?.phase === "presigning" || operation?.phase === "downloading";

  return running ? (
    <button type="button" onClick={() => cancel(objectKey)}>
      Cancel ({operation.progress.percent}%)
    </button>
  ) : (
    <button type="button" onClick={() => void download(objectKey)}>
      Download
    </button>
  );
}
```

The saved name is the `fileName` you pass, then the response's
`Content-Disposition` file name, then the last segment of the key. Pass
`fileSize` when you know it, so progress has a total before the response
arrives:

```ts
await download(objectKey, { fileName: "video.mp4", fileSize: 52_428_800 });
```

`onDownloadStart(key)` runs when the transfer begins and
`onProgress(progress, key)` as bytes arrive. On success, `data` is
`{ fileName, size }`.

A cancelled download ends with an `aborted` outcome, not an error.

---

## Operations and outcomes [#operations-and-outcomes]

Each `download` call records an operation. Read the newest one for a key
with `get(key)`, or render `operations`:

| Field   | `useDownload`                               | `useFetchDownload`                                         |
| ------- | ------------------------------------------- | ---------------------------------------------------------- |
| `phase` | `presigning`, `success`, `error`, `aborted` | `presigning`, `downloading`, `success`, `error`, `aborted` |
| `error` | `APIError` or `null`                        | `APIError` or `null`                                       |
| extra   | `result`: the signed URL response           | `progress`, `fileSize`, and the saved `fileName`           |

`pendingKeys` lists the keys with a running call, and `isPending` is `true`
while there is one. `cancel(key?)` aborts one key or every key, and
`reset(key?)` removes settled operations.

`download` resolves with an outcome and does not throw:

```ts
const outcome = await download(objectKey);

switch (outcome.status) {
  case "success":
    break; // outcome.data
  case "error":
    break; // outcome.error is an APIError
  case "blocked":
    break; // beforeDownload returned false
  case "aborted":
    break; // cancel(key) ran
}
```

`beforeDownload(key)` runs once per call. Return `false` to block it; a throw
fails it with `HOOK_FAILED`. `onError(error, key)` runs when a call fails, and
`onSettled(outcome)` once per call, however it ended. A call that joins a
running call does not run the callbacks again.

### Accept either hook [#accept-either-hook]

Each return has a `mode`: `"navigate"` for `useDownload`, `"fetch"` for
`useFetchDownload`. Type a prop as `AnyDownloadReturn` and narrow on `mode`:

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

function progressOf(download: AnyDownloadReturn, key: string) {
  if (download.mode === "fetch") return download.get(key)?.progress.percent;
  return undefined;
}
```

---

## Type reference [#type-reference]

```ts
import type {
  AnyDownloadReturn,
  DownloadOperation,
  FetchDownloadOperation,
  UseDownloadOptions,
  UseDownloadReturn,
  UseFetchDownloadOptions,
  UseFetchDownloadReturn,
} from "@dimah-s3/react";
```

### UseDownloadOptions [#usedownloadoptions]

<AutoTypeTable
  path="packages/react/src/types/download.ts"
  name="UseDownloadOptions"
/>

### UseDownloadReturn [#usedownloadreturn]

<AutoTypeTable
  path="packages/react/src/types/download.ts"
  name="UseDownloadReturn"
/>

### DownloadOperation [#downloadoperation]

<AutoTypeTable
  path="packages/react/src/types/download.ts"
  name="DownloadOperation"
/>

### UseFetchDownloadOptions [#usefetchdownloadoptions]

<AutoTypeTable
  path="packages/react/src/types/download.ts"
  name="UseFetchDownloadOptions"
/>

### UseFetchDownloadReturn [#usefetchdownloadreturn]

<AutoTypeTable
  path="packages/react/src/types/download.ts"
  name="UseFetchDownloadReturn"
/>

### FetchDownloadOperation [#fetchdownloadoperation]

<AutoTypeTable
  path="packages/react/src/types/download.ts"
  name="FetchDownloadOperation"
/>

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

<Accordions>

<Accordion title="Which hook should I use?">

Use `useDownload` by default. The browser streams the file to disk and your
page holds no bytes. Use `useFetchDownload` when you need progress or
cancel, and the file fits in memory.

</Accordion>

<Accordion title="Can many buttons share one hook?">

Yes. Each key has its own operation, so render each button from `get(key)`.

</Accordion>

<Accordion title="Does useFetchDownload send my client headers?">

It fetches the signed URL with plain `fetch`. On a route with
`download.mode: "proxy"`, the `/file` request sends same-origin cookies but
not the `headers` set on `createS3Client`. Use cookie auth for proxy routes.

</Accordion>

<Accordion title="How do I show a private image inline?">

Use [`useObjectUrl`](https://v2.s3.dimah.dev/docs/react/hooks/object-url). It signs an inline URL,
caches it, and refreshes it before it expires.

</Accordion>

</Accordions>
