Tanstack Start

React

Hooks and prebuilt components from s3delivery/react.

s3delivery/react gives you upload hooks with rich, predictable state, plus two drop-in components. react is an optional peer dependency (>=18).

import { useUploadFiles } from "s3delivery/react";

All hooks build on uploadFiles: parallel files, automatic multipart for large files, client-side constraints, retry with backoff, and an abortable upload. They talk to your route handler, so your key stays on the server.

Which hook?

Use useUploadFile for a single file and useUploadFiles for many — they are the recommended hooks. useUploader is an older, leaner hook kept for compatibility; <UploadButton> / <UploadDropzone> are built on it.

useUploadFiles

Upload multiple files with full state.

import { useUploadFiles } from "s3delivery/react";

function Gallery() {
  const { upload, uploadedFiles, progress, isPending, error } = useUploadFiles({
    maxFiles: 10,
    maxFileSize: 50 * 1024 * 1024,
    allowedFileTypes: ["image/*", "application/pdf"],
    onUploadComplete: (files) => console.log(files),
  });

  return (
    <>
      <input type="file" multiple onChange={(e) => upload(e.target.files!)} />
      {isPending && <progress value={progress} max={100} />}
      {error && <p role="alert">{error.message}</p>}
      {uploadedFiles.map((f) => (
        <a key={f.id} href={f.url}>
          {f.name}
        </a>
      ))}
    </>
  );
}

Return shape

FieldTypeNotes
upload(files, options?) => Promise<S3DeliveryFile[]>Start an upload. options can override visibility.
uploadedFilesS3DeliveryFile[]The files that uploaded successfully (set on success).
progressnumberAggregate progress across all files, 0–100.
progresses{ file: File; progress: UploadProgress }[]Per-file progress for the in-flight batch.
isPendingbooleanAn upload is currently running.
isSuccessbooleanThe last upload finished successfully.
isErrorbooleanThe last upload failed.
isAbortedbooleanThe last upload was aborted.
isSettledbooleanThe last upload finished (success or error).
errorS3DeliveryError | nullThe error from the last failed upload.
reset() => voidReset all state to idle (also aborts any in-flight upload).
control{ abort: () => void; reset: () => void }Imperative handle.

Options

useUploadFiles accepts every uploadFiles option except signal, onProgress, onFileProgress, and onFileComplete (the hook manages those internally), plus lifecycle callbacks:

OptionTypeNotes
onBeforeUpload(files: File[]) => void | PromiseRuns before any URL is requested. Throw to reject.
onProgress(percent: number) => voidAggregate progress on every tick.
onUploadComplete(files: S3DeliveryFile[]) => voidRuns once when every file succeeds.
onError(error: S3DeliveryError) => voidRuns on any failure (validation, network, abort).
onSettled() => voidAlways runs after success or failure.

Per-file progress

const { upload, progresses } = useUploadFiles();

<ul>
  {progresses.map(({ file, progress }) => (
    <li key={file.name}>
      {file.name} — {progress.percent}%
    </li>
  ))}
</ul>;

useUploadFile

The same engine, narrowed to a single file. It defaults maxFiles to 1 and exposes uploadedFile (singular).

import { useUploadFile } from "s3delivery/react";

function Avatar() {
  const { upload, uploadedFile, progress, isPending, control } = useUploadFile({
    maxFileSize: 5 * 1024 * 1024,
    allowedFileTypes: ["image/*"],
  });

  return (
    <>
      <input type="file" onChange={(e) => upload(e.target.files![0])} />
      {isPending && (
        <>
          <progress value={progress} max={100} />
          <button onClick={control.abort}>Cancel</button>
        </>
      )}
      {uploadedFile && <img src={uploadedFile.url} alt="" />}
    </>
  );
}

The return shape matches useUploadFiles with uploadedFile: S3DeliveryFile | null in place of uploadedFiles (and no progresses). The upload function takes a single File and resolves with a single S3DeliveryFile.

Aborting and resetting

control.abort() cancels an in-flight upload — parts and pending retries stop immediately, error.code is ABORTED, and isAborted becomes true. control.reset() (or reset()) clears all state back to idle and also aborts any upload still running.

const { upload, control, isAborted, reset } = useUploadFile();

<button onClick={control.abort}>Cancel</button>;
{
  isAborted && <button onClick={reset}>Try again</button>;
}

Per-call visibility

Override visibility for a single upload without re-configuring the hook:

const { upload } = useUploadFiles({ visibility: "private" });

await upload(files, { visibility: "public" }); // public, just this time

useUploader

The older headless hook. Leaner state; wrap your own UI around it. Prefer useUploadFile / useUploadFiles for new code.

import { useUploader } from "s3delivery/react";

const { upload, isUploading, progress, uploaded, error, reset } = useUploader({
  onSuccess: (files) => console.log(files),
  onError: (e) => console.error(e),
});
FieldType
upload(files) => Promise<S3DeliveryFile[]>
isUploadingboolean
progressnumber (0–100)
uploadedS3DeliveryFile[]
errorError | null
reset() => void

It accepts all uploadFiles options plus onSuccess and onError.

Prebuilt components

<UploadButton> and <UploadDropzone> are zero-config drop-ins built on useUploader. They handle the hidden file input, drag-and-drop, and an uploading/progress label.

import { UploadButton, UploadDropzone } from "s3delivery/react";

<UploadButton accept="image/*" onSuccess={(files) => console.log(files)} />;

<UploadDropzone
  accept="image/*,application/pdf"
  maxFileSize={10 * 1024 * 1024}
  onSuccess={(files) => console.log(files)}
/>;

Both accept every useUploader option plus:

PropDefaultNotes
acceptNative file-input accept filter.
multipletrueAllow selecting more than one file.
disabledfalseDisable the control.
classNameReplace the default inline styles with your own.
styleMerge extra inline styles.
childrenCustom label / dropzone content.

Pass className to opt out of the built-in inline styles and style the control yourself.

On this page