useFileUpload
A hook that manages file selection, validation, previews, and upload progress.
Drop images or PDFs here, up to 2 MB
Installation
Usage
Everything a file input needs that the platform does not give you: drag state, accept and size validation with usable rejection messages, image previews that get revoked, per-file progress, and retry. It renders nothing, so the same logic backs a dropzone, an avatar picker, or a paste handler.
import { useFileUpload } from "@/hooks/use-file-upload";
function Uploader() {
const upload = useFileUpload({
accept: "image/*",
maxSize: 5 * 1024 * 1024,
maxFiles: 4,
});
return (
<div {...upload.getDropzoneProps()} data-dragging={upload.isDragging}>
<input {...upload.getInputProps()} />
<button onClick={upload.openFileDialog}>Choose files</button>
{upload.files.map((item) => (
<p key={item.id}>{item.file.name}</p>
))}
</div>
);
}Uploading
Pass onUpload and the hook drives the transport, tracking status and progress per file. Resolve to
mark it uploaded; throw and the message lands on item.error with a retry available.
const upload = useFileUpload({
autoUpload: true,
onUpload: async (item, { onProgress, signal }) => {
const body = new FormData();
body.append("file", item.file);
await fetch("/api/upload", { method: "POST", body, signal });
onProgress(1);
},
});signal aborts when the file is removed or the component unmounts, so a cancelled upload does not
keep streaming. For real byte-level progress use XMLHttpRequest — fetch cannot report upload
progress — and call onProgress with a fraction between 0 and 1.
Rejections
Files that fail validation never enter files. They arrive as rejections, each with a reason
("type", "size", "count", "duplicate") and a message you can render as-is.
{upload.rejections.map((rejection) => (
<p key={rejection.file.name} className="text-destructive">{rejection.message}</p>
))}Duplicates are matched on name, size, and last-modified time — the closest thing to a file identity the browser offers.
Options
| Option | Description |
|---|---|
accept | Same syntax as the accept attribute: "image/*,.pdf". Enforced on drop too, which the attribute alone does not do. |
maxSize | Per-file limit in bytes. |
maxFiles | Maximum number of accepted files. |
multiple | When false, a new selection replaces the current file. |
disabled | Ignore selection and drops. |
onUpload | Upload transport. Receives onProgress and an AbortSignal. |
autoUpload | Upload as soon as files are accepted. |
onFilesChange | Called with the full list after every change. |
onRejected | Called with the rejections from a single selection. |
Returns
| Value | Description |
|---|---|
files | UploadFile[] — id, file, status, progress, error, previewUrl. |
rejections | Rejections from the most recent selection. |
isDragging / isUploading | Derived flags for styling. |
addFiles / removeFile / clearFiles | Imperative list control — addFiles also accepts a paste or drop FileList. |
uploadAll / retryFile | Trigger uploads when autoUpload is off. |
openFileDialog / inputRef | Open the native picker from your own trigger. |
getInputProps / getDropzoneProps | Prop getters for the hidden input and the drop target. |
previewUrl is an object URL created only for image/* files and revoked when the file is removed
or the component unmounts, so previews do not leak.
The formatBytes helper is exported alongside the hook for rendering file sizes.