Batch Upload

Collect multiple files and upload them all at once in a single request.

Add files, then upload them together as one batch.

import { useCallback, useRef, useState } from "react";
import {
UplofilePreview,
UplofileRoot,
UplofileTrigger,
type UploadStatus,
} from "@/components/ui/uplofile";
import { BatchFileItem } from "./batchupload.batchfileitem.demo.tsx";
type BatchStatus = Extract<
UploadStatus,
"idle" | "uploading" | "done" | "error"
>;
type PendingUpload = {
file: File;
resolve: (value: { id: string; url: string }) => void;
reject: (reason: Error) => void;
setProgress?: (progress: number) => void;
};
export default function BatchUploadDemo() {
const [status, setStatus] = useState<BatchStatus>("idle");
const [progress, setProgress] = useState(0);
const pendingUploads = useRef(new Map<File, PendingUpload>());
const queueFile = useCallback(
(
file: File,
signal: AbortSignal,
setFileProgress?: (progress: number) => void,
) =>
new Promise<{ id: string; url: string }>((resolve, reject) => {
const pending = { file, resolve, reject, setProgress: setFileProgress };
pendingUploads.current.set(file, pending);
signal.addEventListener(
"abort",
() => {
pendingUploads.current.delete(file);
reject(new Error("Upload cancelled"));
},
{ once: true },
);
}),
[],
);
const uploadBatch = useCallback(async (files: File[]) => {
if (files.length === 0) return;
setStatus("uploading");
setProgress(0);
try {
for (const [index, file] of files.entries()) {
const pending = pendingUploads.current.get(file);
if (!pending) continue;
await new Promise<void>((resolve) => {
let fileProgress = 0;
const interval = window.setInterval(() => {
fileProgress = Math.min(fileProgress + 10, 100);
pending.setProgress?.(fileProgress);
setProgress(
Math.round(((index + fileProgress / 100) / files.length) * 100),
);
if (fileProgress === 100) {
window.clearInterval(interval);
resolve();
}
}, 80);
});
pending.resolve({
id: file.name,
url: `https://example.com/uploads/${encodeURIComponent(file.name)}`,
});
pendingUploads.current.delete(file);
}
setStatus("done");
} catch {
setStatus("error");
}
}, []);
return (
<UplofileRoot upload={queueFile} multiple accept="*/*">
<UplofilePreview
render={({ items }) => {
const files = items.flatMap((item) => (item.file ? [item.file] : []));
return (
<div className="space-y-4">
<div className="rounded-xl border border-border bg-muted/30 p-4">
<div className="flex flex-wrap gap-2">
<UplofileTrigger asChild>
<button className="rounded-lg border border-border bg-background px-3 py-2 text-sm font-medium">
Add files
</button>
</UplofileTrigger>
<button
type="button"
onClick={() => uploadBatch(files)}
disabled={files.length === 0 || status === "uploading"}
className="rounded-lg bg-primary px-3 py-2 text-sm font-semibold text-primary-foreground disabled:opacity-50"
>
Upload batch{files.length ? ` (${files.length})` : ""}
</button>
</div>
{status !== "idle" && (
<div className="mt-4 space-y-2">
<div className="flex justify-between text-xs text-muted-foreground">
<span>{status}</span>
<span>{progress}%</span>
</div>
<div className="h-2 overflow-hidden rounded-full bg-secondary">
<div
className="h-full bg-primary transition-all"
style={{ width: `${progress}%` }}
/>
</div>
</div>
)}
</div>
<div className="divide-y overflow-hidden rounded-xl border border-border bg-card">
{items.length === 0 ? (
<p className="p-8 text-center text-sm text-muted-foreground">
Add files, then upload them together as one batch.
</p>
) : (
items.map((item) => (
<BatchFileItem key={item.uid} item={item} />
))
)}
</div>
</div>
);
}}
/>
</UplofileRoot>
);
}

Notes

  • The upload function defers resolution until a batch trigger resolves all pending promises.
  • A Upload All button drains the queue and sends all files as one batch.
  • useUplofile is not needed — pending state is tracked outside the library via a ref.
  • Abort handling works normally: cancelling a pending file removes it from the batch queue.