react-chunk-uploader
v0.1.0
Published
Resumable, chunked file uploads for React with progress, retry, pause/resume, drag-and-drop, and a backend-agnostic API.
Downloads
176
Maintainers
Readme
react-chunk-uploader
Resumable, chunked file uploads for React — with progress, retry with exponential backoff, pause/resume, drag-and-drop, and a backend-agnostic request hook.
Zero runtime dependencies. Tree-shakable. Fully typed. Works with any backend (S3 multipart, your own Express/Fastify/Nest endpoint, Cloudflare R2, GCS, etc.).
Why
Most React upload libraries either ship a heavy UI you can't customize, lock you into one protocol, or stop at "drop a file." Real apps need:
- Chunked uploads for large files so a flaky network doesn't restart from zero.
- Resume across page reloads (LocalStorage-backed progress).
- Retry with backoff because transient 5xx and network errors are normal.
- Pause / cancel because users hit "stop" and reload.
- Headless so the UI stays yours.
react-chunk-uploader does just that.
Install
npm install react-chunk-uploader
# or
pnpm add react-chunk-uploader
# or
yarn add react-chunk-uploaderPeer dependency: react >= 17.
Quickstart
import { useUpload } from 'react-chunk-uploader'
export function Uploader() {
const { files, getInputProps, getDropzoneProps, isDragActive, overallProgress } =
useUpload({
endpoint: '/api/upload',
chunkSize: 5 * 1024 * 1024, // 5 MB
concurrency: 3,
})
return (
<div {...getDropzoneProps()} style={{ border: '2px dashed', padding: 24 }}>
<input {...getInputProps()} />
<p>{isDragActive ? 'Drop here' : 'Drag files or click to upload'}</p>
<progress value={overallProgress} max={100} />
<ul>
{files.map((f) => (
<li key={f.id}>
{f.file.name} — {f.status} — {f.progress.toFixed(0)}%
</li>
))}
</ul>
</div>
)
}That's it — large files are chunked, retried on failure, and resumable across reloads.
Custom request (any backend)
If you don't have a POST /api/upload route, pass your own request function. It is called once per chunk and is fully backend-agnostic.
useUpload({
request: async ({ file, chunk, chunkIndex, totalChunks, uploadId, signal }) => {
const form = new FormData()
form.append('chunk', chunk)
form.append('uploadId', uploadId)
form.append('chunkIndex', String(chunkIndex))
form.append('totalChunks', String(totalChunks))
form.append('fileName', file.name)
const res = await fetch('/api/my-upload', {
method: 'POST',
body: form,
signal,
})
if (!res.ok) throw new Error(`Chunk ${chunkIndex} failed: ${res.status}`)
// Optional: tell the uploader the server has assembled the final file.
const data = (await res.json()) as { complete?: boolean; url?: string }
return { isComplete: data.complete, result: data }
},
})This pattern works for S3 multipart, GCS resumable, Cloudflare R2, tus, or your own protocol.
API
useUpload(options)
| Option | Type | Default | Description |
| -------------- | ------------------------------------------------------------- | ------------- | ---------------------------------------------------------------------- |
| endpoint | string | — | URL for the default chunk POST. Used only when request is not given. |
| request | (args) => Promise<{ isComplete?, result? } \| void> | — | Custom per-chunk request function. Overrides endpoint. |
| chunkSize | number | 5 MB | Bytes per chunk. |
| concurrency | number | 3 | Parallel chunk uploads per file. |
| retry | { attempts, initialDelay, maxDelay, factor, jitter } \| false | 3 attempts | Retry config or false to disable. |
| headers | Record<string,string> \| () => Record<string,string> | — | Headers added to the default request. |
| autoStart | boolean | true | Start upload as soon as a file is added. |
| persistKey | string \| false | 'rcu:state' | LocalStorage key for resume; false disables persistence. |
| multiple | boolean | true | Allow multiple files at once. |
| accept | string | — | Standard input accept filter (e.g. 'image/*,.pdf'). |
| onSuccess | (upload) => void | — | Per-file success callback. |
| onError | (upload, error) => void | — | Per-file error callback. |
Returns
{
files: FileUpload[] // current state of all uploads
upload: (input) => FileUpload[] // start uploading File | File[] | FileList
pause: (id) => void
resume: (id) => void
cancel: (id) => void
retry: (id) => void // retry a failed upload
remove: (id) => void
clear: () => void
isUploading: boolean
overallProgress: number // 0–100
isDragActive: boolean
getDropzoneProps: () => DropzoneProps
getInputProps: () => InputProps
}FileUpload
{
id: string
file: File
status: 'pending' | 'uploading' | 'paused' | 'success' | 'error' | 'canceled'
bytesUploaded: number
totalBytes: number
progress: number // 0–100
error?: Error
result?: unknown // whatever your `request` returned
startedAt?: number
finishedAt?: number
}Server contract (default endpoint mode)
When you pass endpoint instead of request, every chunk is sent as a POST with the raw chunk bytes as the HTTP body and these headers:
| Header | Description |
| ---------------- | ------------------------------------------------------ |
| X-Upload-Id | Stable ID per file (derived from contents). |
| X-Chunk-Index | 0-based chunk index. |
| X-Chunk-Count | Total chunk count. |
| X-File-Name | URL-encoded original file name. |
| X-File-Size | Original file size in bytes. |
| X-File-Type | MIME type. |
The server should respond with 2xx, and ideally JSON { "complete": true, "url": "..." } on the final chunk. See examples/server.js for a small Express implementation that reassembles chunks on disk.
If you'd rather use multipart FormData, S3 multipart, or any other protocol, pass a custom request function instead — see the example above.
Resume across page reloads
Progress is persisted in LocalStorage. When the user picks the same file again after a reload, already-uploaded chunks are skipped automatically. Set persistKey: false to disable, or pass a custom storage adapter (e.g. IndexedDB).
Browser support
Modern evergreen browsers. Requires File, Blob.slice, AbortController, crypto.subtle.digest (HTTPS or localhost).
License
MIT
