mwui
GitHub

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 XMLHttpRequestfetch 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

OptionDescription
acceptSame syntax as the accept attribute: "image/*,.pdf". Enforced on drop too, which the attribute alone does not do.
maxSizePer-file limit in bytes.
maxFilesMaximum number of accepted files.
multipleWhen false, a new selection replaces the current file.
disabledIgnore selection and drops.
onUploadUpload transport. Receives onProgress and an AbortSignal.
autoUploadUpload as soon as files are accepted.
onFilesChangeCalled with the full list after every change.
onRejectedCalled with the rejections from a single selection.

Returns

ValueDescription
filesUploadFile[]id, file, status, progress, error, previewUrl.
rejectionsRejections from the most recent selection.
isDragging / isUploadingDerived flags for styling.
addFiles / removeFile / clearFilesImperative list control — addFiles also accepts a paste or drop FileList.
uploadAll / retryFileTrigger uploads when autoUpload is off.
openFileDialog / inputRefOpen the native picker from your own trigger.
getInputProps / getDropzonePropsProp 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.