@alsocoder/apna-upload
Provider-free React file upload component with drag-and-drop, progress, cancel, retry, and validation.
Install
npm install @alsocoder/apna-upload
Peer dependencies: react, react-dom.
Optional: pdfjs-dist for PDF first-page thumbnails (enablePdfPreview).
Setup
import "@alsocoder/apna-upload/styles.css"
Basic usage
import { ApnaUpload } from "@alsocoder/apna-upload"
async function uploadFile(file: File, { signal, onProgress }) {
const body = new FormData()
body.append("file", file)
const response = await fetch("/api/upload", {
method: "POST",
body,
signal,
})
if (!response.ok) {
throw new Error("Upload failed")
}
onProgress(100)
return response.json()
}
export function ResumeField() {
const [resume, setResume] = useState("")
return (
<ApnaUpload
label="Resume"
accept=".pdf"
upload={uploadFile}
transformResult={(result) => result.file.path}
value={resume}
onChange={setResume}
/>
)
}
Multiple files
<ApnaUpload
multiple
maxFiles={5}
accept="image/*"
upload={uploadFile}
value={images}
onChange={setImages}
/>
Result modes
resultMode |
onChange value |
|---|---|
"url" (default) |
string or string[] (via transformResult) |
"file" |
File or File[] |
Hooks and callbacks
| Prop | Purpose |
|---|---|
beforeUpload |
Transform or reject a file before upload (null = reject). Use for crop, compression, watermark, EXIF fix. |
transformResult |
Map API response to field value. Default extracts { url } or a string. |
onUploading |
Called during upload with (file, progress) — use for analytics or custom progress UI |
onUploaded |
Called per file after successful upload |
onRemoved |
Called when user removes a completed file |
onError |
Per-file error callback (wire your own toast) |
Validation
Built-in rules: accept, maxSize, maxFiles.
For custom rules, use validate:
<ApnaUpload
accept="image/*"
validate={(file) => {
if (!file.name.endsWith(".png")) {
return "Only PNG allowed"
}
return true
}}
upload={uploadFile}
/>
Return true to allow, or a string error message to reject. Supports async validators.
Disabled and read-only
| Prop | Behavior |
|---|---|
disabled |
No browse, drop, paste, cancel, retry, or remove |
readOnly |
Files visible but no interactions (same as disabled for actions) |
<ApnaUpload disabled upload={uploadFile} />
<ApnaUpload readOnly value={url} upload={uploadFile} />
Custom preview
Override the default thumbnail with renderPreview:
<ApnaUpload
renderPreview={(file, item) => (
<div className="my-card">
<strong>{file.name}</strong>
<span>{item.progress}%</span>
</div>
)}
upload={uploadFile}
/>
Headless hook
Use useApnaUpload() for custom UI — same engine as the component:
import { useApnaUpload } from "@alsocoder/apna-upload"
const { items, enqueueFiles, cancelItem, retryItem, removeItem } = useApnaUpload({
upload: uploadFile,
validate: (file) => file.size < 5_000_000 || "Max 5 MB",
onUploading: (file, progress) => track("upload", { file: file.name, progress }),
})
Architecture
ApnaUpload (UI)
├── Upload Engine — queue, progress, cancel, retry
├── Validation — accept, maxSize, maxFiles, validate()
├── Preview — image/PDF thumbnails, renderPreview
├── Drag & Drop — dropzone
├── Paste — clipboard files
├── Reorder — drag sort (multiple)
└── UI — dropzone + file list
useApnaUpload() exposes the engine layer without UI.
Features
| Feature | Status |
|---|---|
| Drag & drop | Yes |
| Browse | Yes |
| Multiple | Yes |
| Preview (image) | Yes |
| PDF preview | Yes (enablePdfPreview + optional pdfjs-dist) |
| Progress | Yes |
| Retry | Yes |
| Cancel | Yes |
beforeUpload |
Yes |
onUploading |
Yes |
validate (custom) |
Yes |
renderPreview |
Yes |
disabled / readOnly |
Yes |
onUploaded / onRemoved |
Yes |
useApnaUpload() hook |
Yes |
| Max files / max size / accept | Yes |
resultMode url | file |
Yes |
| AbortController | Yes |
| Paste upload (Ctrl+V) | Yes (enablePaste, default on) |
| Reorder (drag) | Yes (enableReorder + multiple) |
| Chunk upload | Adapter contract — see below |
Chunk upload (adapter contract)
For large files, your upload function may receive optional chunk metadata:
type ApnaUploadFn = (
file: File,
ctx: {
signal: AbortSignal
onProgress: (percent: number) => void
chunk?: { index: number; total: number; slice: Blob }
}
) => Promise<unknown>
Implement multipart/resumable logic in your adapter — the component does not ship S3/Cloudinary adapters.
Custom upload adapters
Implement any backend in the upload function:
- S3 presigned URL
- Cloudinary unsigned upload
- Firebase Storage
- Supabase storage
- Your REST API
Return any JSON shape and use transformResult to map it to your form value.
License
MIT