useUpload
Headless React hook for single and multi-file S3 uploads with progress and retries.
useUpload manages the complete client-side upload lifecycle: file selection, validation against server constraints, presigned URL acquisition, chunked progress tracking, and final server confirmation.
"use client";
import { useUpload } from "@dimah-s3/react";
export function AvatarUploader() {
const upload = useUpload({
route: "avatar",
onSuccess: (results) => {
console.log("Uploaded object key:", results[0]?.key);
},
});
return (
<div
{...upload.getRootProps()}
className="border border-dashed p-6 rounded-lg text-center cursor-pointer hover:border-primary transition-colors"
>
<input {...upload.getInputProps()} />
{upload.isUploading ? (
<p className="text-sm font-medium">
Uploading: {upload.progress.percent}%
</p>
) : (
<p className="text-sm text-muted-foreground">
Click or drop avatar here
</p>
)}
</div>
);
}File selection and intake
useUpload provides native bindings for drag-and-drop surfaces and file input elements:
getRootProps(): Spread onto your container element to bind drag-and-drop and click-to-browse handlers.getInputProps(): Spread onto a hidden<input type="file" />.open(): Programmatically trigger the OS file dialog from a custom button.handleFiles(files): Manually submitFile,File[], orFileList(e.g. from clipboard paste, webcam capture, or canvas blob).
const upload = useUpload({
route: "avatar",
noClick: true, // Prevents container click from opening file picker
});
return (
<div {...upload.getRootProps()}>
<input {...upload.getInputProps()} />
<button type="button" onClick={() => upload.open()}>
Choose File
</button>
</div>
);Multi-file batch uploads
Set maxFiles to allow multiple file selections. You can control concurrency with concurrentFiles:
const upload = useUpload({
route: "avatar",
maxFiles: 5,
concurrentFiles: 2,
onFileSuccess: (file, result) => {
console.log(`Uploaded ${file.name} to ${result.key}`);
},
onSuccess: (results) => {
console.log("All uploads completed:", results);
},
});Progress and state tracking
The hook surfaces detailed reactive state for UI rendering:
phase: Current upload phase (idle|validating|presigning|uploading|finalizing|success|error).progress: Aggregate progress object containingloaded,total,percent, and instantaneousspeed(bytes/sec).files: Per-file state array containing individual progress, status (pending,uploading,success,error), andpreviewUrl.file: Convenience shorthand forfiles[0]whenmaxFilesis 1.isUploading: Boolean flag indicating active byte transfer (phase === "uploading").isPending: Boolean flag indicating in-flight operation from presign to confirmation.
Type reference
import type {
UseUploadOptions,
UseUploadReturn,
UploadProgress,
UploadResult,
UploadFileState,
} from "@dimah-s3/react";UseUploadOptions
Prop
Type
UseUploadReturn
Prop
Type
Frequently asked questions
useUpload still works. The server enforces type and size on presign.
policy.catalogStatus is "error" and policy.catalogError is set so
you can toast or pass explicit accept / maxFileSize on the hook.
In development, a one-time console warning is also printed.
Custom backends that do not implement GET /routes should pass those
constraints on useUpload and ignore the catalog status.
Use previewUrl on individual items in the upload.files array or create an object URL from upload.file:
export function AvatarWithPreview() {
const upload = useUpload({ route: "avatar" });
const preview = upload.file?.previewUrl;
return (
<div>
<div {...upload.getRootProps()}>
<input {...upload.getInputProps()} />
{preview ? (
<img
src={preview}
alt="Selected avatar"
className="size-20 rounded-full object-cover"
/>
) : (
<button type="button">Select avatar</button>
)}
</div>
{upload.isUploading && <span>{upload.progress.percent}%</span>}
</div>
);
}Pass static uploadOptions or a dynamic getUploadOptions function:
const upload = useUpload({
route: "avatar",
getUploadOptions: (file) => ({
metadata: {
originalName: file.name,
uploadedAt: new Date().toISOString(),
},
}),
});Use cancel() to abort the upload and clean up temporary parts, or detach() when an uploadStore is configured to pause for later resumption:
const upload = useUpload({
route: "avatar",
uploadStore: localStorageStore,
});
// Abort immediately
upload.cancel();
// Soft-stop to resume on next visit
upload.detach();