useUpload

Overview

useUpload is a lightweight, production-ready React hook that lets you upload files directly from the browser using presigned URLs. It features built-in progress tracking, retry logic, file validation, and robust cancellation.

It is ideal for uploading large files to services like S3, Cloudflare R2, or GCS without proxying files through your backend.


Installation

npm install upload-with-progress

Basic Usage

import React from "react";
import { useUpload, UploadError } from "upload-with-progress";
 
function FileUploader() {
  const { upload, progress, status, error, abort } = useUpload({
    maxFileSize: 10 * 1024 * 1024, // 10 MB limit
    allowedTypes: ["image/*"],
    retries: 2, // Auto-retry on network errors or 5xx
  });
 
  const handleFileChange = async (e: React.ChangeEvent<HTMLInputElement>) => {
    const file = e.target.files?.[0];
    if (!file) return;
 
    try {
      // The `upload` function returns the metadata from your backend
      const uploadedMeta = await upload(file, async () => {
        // Fetch the presigned URL from your API
        const response = await fetch("/api/get-presigned-url", {
          method: "POST",
          body: JSON.stringify({ name: file.name, type: file.type }),
        });
 
        // Expected response format: { presignedUrl: string, meta: any }
        return response.json();
      });
 
      console.log("Upload successful!", uploadedMeta);
    } catch (err) {
      if (err instanceof UploadError) {
        if (err.code === "ABORTED") console.log("User cancelled");
        else console.error(`Upload error: ${err.message}`);
      }
    }
  };
 
  return (
    <div>
      <input type="file" onChange={handleFileChange} />
 
      {status === "uploading" && (
        <div className="progress-bar">
          <p>Uploading... {progress}%</p>
          <progress value={progress} max="100" />
          <button onClick={abort}>Cancel</button>
        </div>
      )}
 
      {status === "error" && error && <p className="error">Error: {error.message}</p>}
    </div>
  );
}

Hook Options

useUpload(options) takes an optional object to configure its behavior:

OptionTypeDefaultDescription
maxFileSizenumberInfinityMax file size in bytes. Rejects the file upfront if it exceeds this limit.
allowedTypesstring[]undefinedAllowed MIME types. Supports wildcards (e.g., ["image/png", "video/*"]).
timeoutnumber0Per-upload timeout in milliseconds. Set to 0 to disable.
retriesnumber0Auto-retries for transient failures (Network errors, Timeouts, HTTP 429/5xx).
retryDelaynumber1000Base delay for exponential backoff in milliseconds. Max backoff caps at 30s.
headersRecord<string, string>undefinedCustom HTTP headers sent during the PUT request.
signalAbortSignalundefinedNative AbortSignal for external cancellation (e.g., via AbortController).
onProgress(pct: number) => voidundefinedCallback fired on progress updates. Ideal for external stores (e.g., Zustand) without triggering react renders.

Returned State

The hook returns an object with the following properties:

PropertyTypeDescription
uploadFunctionThe async function to trigger the upload. Requires the File and a callback returning the presigned URL.
progressnumberA value from 0 to 100 representing the upload progress.
statusUploadStatus"idle" | "uploading" | "success" | "error"
isUploadingbooleanSyntactic sugar for status === "uploading".
errorUploadError | nullSet if the upload encounters an unrecoverable error.
abort() => voidCancels the upload immediately. Triggers an UploadError with code "ABORTED".
reset() => voidResets the hook state back to "idle" and progress = 0.

Backend Requirements

The backend must return an object containing a presigned upload URL and metadata about the uploaded file.

{
  presignedUrl: string;
  meta: any; // The metadata you want to return
}

Example backend response:

{
  "presignedUrl": "https://bucket.s3.amazonaws.com/...",
  "meta": {
    "finalFileUrl": "https://cdn.example.com/video.mp4",
    "objectKey": "uploads/video.mp4"
  }
}