@mshafiqyajid/react-file-upload
Headless file upload hook and styled component. Revamped dropzone UI with customisable labels, drag-and-drop, image previews, custom preview renderer, validation callbacks, multi-file. Async uploader with progress, abort, retry — concurrency-aware queue.
Playground #
Drop files here
or
Props
TSX
import { FileUploadStyled } from "@mshafiqyajid/react-file-upload/styled";
import "@mshafiqyajid/react-file-upload/styles.css";
<FileUploadStyled />Install #
npm install @mshafiqyajid/react-file-upload Quick start #
import { FileUploadStyled } from "@mshafiqyajid/react-file-upload/styled";
import "@mshafiqyajid/react-file-upload/styles.css";
<FileUploadStyled
multiple
accept="image/*"
maxSize={5242880}
showPreview
onFiles={(result) => console.log(result.files, result.errors)}
/> Async uploader #
Pass an uploader to upload accepted files automatically. Each file becomes an UploadItem with status (queued | uploading | success | error | aborted) and a progress you control by calling ctx.onProgress(fraction). The hook runs uploads concurrently up to concurrency (default 3) and respects ctx.signal for abort.
<FileUploadStyled
multiple
uploader={async (file, { signal, onProgress }) => {
const xhr = new XMLHttpRequest();
xhr.upload.onprogress = (e) => e.lengthComputable && onProgress(e.loaded / e.total);
signal.addEventListener("abort", () => xhr.abort());
return await new Promise((resolve, reject) => {
xhr.onload = () => resolve(JSON.parse(xhr.responseText));
xhr.onerror = () => reject(new Error("upload failed"));
xhr.open("POST", "/api/upload");
const fd = new FormData();
fd.append("file", file);
xhr.send(fd);
});
}}
concurrency={3}
onUpload={(item) => console.log(item.status, item.progress)}
/> Headless consumers also get uploads, retryUpload(id), abortUpload(id), and abortAll().
Headless #
import { useFileUpload } from "@mshafiqyajid/react-file-upload";
const {
getRootProps,
getInputProps,
isDragOver,
files,
removeFile,
open,
} = useFileUpload({
multiple: true,
accept: "image/*",
});
return (
<div {...getRootProps()} data-drag-over={isDragOver} className="dz">
<input {...getInputProps()} />
<button onClick={open}>Browse</button>
<ul>
{files.map((file, i) => (
<li key={i}>
{file.name}
<button onClick={() => removeFile(i)}>×</button>
</li>
))}
</ul>
</div>
); API #
| Prop | Type | Default | Description |
|---|---|---|---|
| variant | "dropzone" | "button" | "dropzone" | Upload widget style |
| multiple | boolean | false | Allow multiple files |
| accept | string | — | Accepted file types (MIME or extension) |
| maxSize | number | — | Max file size in bytes |
| maxFiles | number | — | Max number of files |
| showPreview | boolean | true | Show image previews |
| size | "sm" | "md" | "lg" | "md" | Component size |
| onFiles | (result) => void | — | Called when files change. result.accepted, result.rejected |
| uploader | (file, ctx) => Promise<T> | — | Async upload function. ctx: { signal, onProgress }. |
| autoUpload | boolean | true when uploader provided | Start uploading newly accepted files automatically |
| concurrency | number | 3 | Max simultaneous uploads |
| onUpload | (item) => void | — | Called whenever an upload item changes state |
| disabled | boolean | false | Disable interaction |
| uploadText | string | "Drop files here" | Main dropzone label |
| browseText | string | "browse" | Browse link text |
| renderPreview | (file, onRemove, upload?) => ReactNode | — | Custom preview renderer; receives the matching UploadItem when an uploader is provided |