2.0 beta — new releases can include breaking changes. Stable 1.x docs
dimah-s3v2.0.0-beta.0

useDownload

Headless React hooks for presigned downloads — useDownload hands the URL to the browser, useFetchDownload fetches the bytes with progress.

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.

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.


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.

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(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.

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 fetches the bytes in the page and saves them as a file. It reports progress and can be cancelled.

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:

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

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

FielduseDownloaduseFetchDownload
phasepresigning, success, error, abortedpresigning, downloading, success, error, aborted
errorAPIError or nullAPIError or null
extraresult: the signed URL responseprogress, 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:

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

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

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

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

UseDownloadOptions

Prop

Type

UseDownloadReturn

Prop

Type

DownloadOperation

Prop

Type

UseFetchDownloadOptions

Prop

Type

UseFetchDownloadReturn

Prop

Type

FetchDownloadOperation

Prop

Type

Frequently asked questions

On this page