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-progressBasic 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:
| Option | Type | Default | Description |
|---|---|---|---|
maxFileSize | number | Infinity | Max file size in bytes. Rejects the file upfront if it exceeds this limit. |
allowedTypes | string[] | undefined | Allowed MIME types. Supports wildcards (e.g., ["image/png", "video/*"]). |
timeout | number | 0 | Per-upload timeout in milliseconds. Set to 0 to disable. |
retries | number | 0 | Auto-retries for transient failures (Network errors, Timeouts, HTTP 429/5xx). |
retryDelay | number | 1000 | Base delay for exponential backoff in milliseconds. Max backoff caps at 30s. |
headers | Record<string, string> | undefined | Custom HTTP headers sent during the PUT request. |
signal | AbortSignal | undefined | Native AbortSignal for external cancellation (e.g., via AbortController). |
onProgress | (pct: number) => void | undefined | Callback 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:
| Property | Type | Description |
|---|---|---|
upload | Function | The async function to trigger the upload. Requires the File and a callback returning the presigned URL. |
progress | number | A value from 0 to 100 representing the upload progress. |
status | UploadStatus | "idle" | "uploading" | "success" | "error" |
isUploading | boolean | Syntactic sugar for status === "uploading". |
error | UploadError | null | Set if the upload encounters an unrecoverable error. |
abort | () => void | Cancels the upload immediately. Triggers an UploadError with code "ABORTED". |
reset | () => void | Resets 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"
}
}