Getting started
react-upload-kit is a headless, adapter-based file upload toolkit for React. It handles the hard parts — drag & drop, validation, concurrency, retry, cancellation, previews — while you own the UI.
Most upload libraries either ship opinionated markup that fights your design system, or leave you wiring low-level browser APIs by hand. This library sits in between: composable hooks, optional renderless components, and a single UploadAdapter you control.
Installation
npm install react-upload-kitPeer dependency: React 18 or later. There are no other runtime dependencies.
Two entrypoints
// Hooks, core utilities, and types
import { useUploader, useDropzone, usePaste, useFilePreview } from 'react-upload-kit';
// Optional renderless components
import { Dropzone, FileList, UploadTrigger } from 'react-upload-kit/components';The package is dual CJS/ESM, sideEffects: false, and tree-shakeable. Import only what you use.
Minimal example
The adapter is the only required piece of infrastructure. Everything else is optional.
import { useUploader, useDropzone } from 'react-upload-kit';
const uploadAdapter = async (file, { onProgress, signal }) => {
const body = new FormData();
body.append('file', file);
const xhr = new XMLHttpRequest();
return new Promise((resolve, reject) => {
signal.addEventListener('abort', () => xhr.abort());
xhr.upload.addEventListener('progress', (e) => {
if (e.lengthComputable) {
onProgress(Math.round((e.loaded / e.total) * 100));
}
});
xhr.addEventListener('load', () => {
if (xhr.status >= 200 && xhr.status < 300) {
resolve(JSON.parse(xhr.responseText));
} else {
reject(new Error(`Upload failed: ${xhr.status}`));
}
});
xhr.addEventListener('error', () => reject(new Error('Network error')));
xhr.open('POST', '/api/upload');
xhr.send(body);
});
};
function FileUploader() {
const uploader = useUploader({
adapter: uploadAdapter,
accept: ['image/*', '.pdf'],
maxFileSize: 10 * 1024 * 1024,
maxFiles: 5,
autoUpload: true,
concurrency: 3,
maxRetries: 2,
});
const dropzone = useDropzone({
onDrop: uploader.addFiles,
accept: ['image/*', '.pdf'],
});
return (
<div>
<div {...dropzone.getRootProps()}>
<input {...dropzone.getInputProps()} />
{dropzone.isDragActive
? 'Drop files here…'
: 'Drag & drop files, or click to select'}
</div>
{uploader.files.map((file) => (
<div key={file.id}>
<span>{file.file.name}</span>
<span>{file.progress}%</span>
<span>{file.status}</span>
{file.status === 'uploading' && (
<button onClick={() => uploader.cancelFile(file.id)}>Cancel</button>
)}
{file.status === 'error' && (
<button onClick={() => uploader.retryFile(file.id)}>Retry</button>
)}
<button onClick={() => uploader.removeFile(file.id)}>Remove</button>
</div>
))}
</div>
);
}How the pieces fit
| Piece | Role |
|---|---|
| Adapter | Your upload function. Receives a File, reports progress, respects abort. |
useUploader | Orchestrator: file state, validation, queue, retry, cancel. |
useDropzone | Prop-getters for a drag & drop / click-to-select surface. |
usePaste | Clipboard images into the same addFiles pipeline. |
useFilePreview | Object URL thumbnails with automatic cleanup. |
| Components | Thin render-prop wrappers if you prefer a declarative API. |
Keep validation rules on useUploader as the source of truth. Mirror accept (and size limits, if you want live drag feedback) on useDropzone. Files added via paste or a custom button still go through the uploader.
Next
- Write an adapter — XHR, fetch, S3, and the contract
- Validation — MIME, size, count, custom rules
- File lifecycle — statuses, retry, cancel
useUploaderAPI