@samithahansaka/dropup
v1.0.0
Published
Lightweight, headless React file upload library with progress tracking, chunked uploads, and cloud storage support
Downloads
326
Maintainers
Readme
dropup
A lightweight, headless React file upload library with progress tracking, chunked uploads, and cloud storage support.
Features
- Headless - Full control over UI with hooks-based API
- Drag & Drop - Built-in drag-and-drop support with
getDropProps() - Progress Tracking - Real-time upload progress for each file
- Chunked Uploads - Split large files into chunks for reliable uploads
- Resumable Uploads - tus protocol support for resumable uploads
- Cloud Storage - Pre-built helpers for S3, GCS, and Azure Blob
- Image Processing - Built-in compression and preview generation
- Cross-Platform - Works with React DOM and React Native
- SSR Safe - Compatible with Next.js and other SSR frameworks
- TypeScript - Full TypeScript support with comprehensive types
- Tiny Bundle - Core is ~10KB gzipped, tree-shakeable
Installation
npm install @samithahansaka/dropup
# or
yarn add @samithahansaka/dropup
# or
pnpm add @samithahansaka/dropupQuick Start
import { useDropup } from '@samithahansaka/dropup'
function FileUploader() {
const { files, actions, state, getDropProps, getInputProps } = useDropup({
accept: 'image/*',
maxSize: 10 * 1024 * 1024, // 10MB
multiple: true,
upload: {
url: '/api/upload',
},
})
return (
<div>
<div
{...getDropProps()}
style={{
border: state.isDragActive ? '2px dashed blue' : '2px dashed gray',
padding: 40,
textAlign: 'center',
}}
>
<input {...getInputProps()} />
{state.isDragActive ? (
<p>Drop files here...</p>
) : (
<p>Drag & drop files here, or click to select</p>
)}
</div>
{files.map((file) => (
<div key={file.id}>
<span>{file.name}</span>
<span>{file.progress}%</span>
<span>{file.status}</span>
<button onClick={() => actions.remove(file.id)}>Remove</button>
</div>
))}
<button onClick={() => actions.upload()} disabled={state.isUploading}>
Upload All
</button>
</div>
)
}API Reference
useDropup(options)
The main hook for file upload functionality.
Options
| Option | Type | Default | Description |
|--------|------|---------|-------------|
| accept | string \| string[] | - | Accepted file types (e.g., 'image/*', ['.pdf', '.doc']) |
| maxSize | number | - | Maximum file size in bytes |
| minSize | number | - | Minimum file size in bytes |
| maxFiles | number | - | Maximum number of files |
| multiple | boolean | false | Allow multiple file selection |
| disabled | boolean | false | Disable the dropzone |
| autoUpload | boolean | false | Automatically upload files when added |
| upload | UploadOptions | - | Upload configuration |
| onFilesAdded | (files: DropupFile[]) => void | - | Callback when files are added |
| onUploadComplete | (file: DropupFile) => void | - | Callback when a file upload completes |
| onUploadError | (file: DropupFile, error: Error) => void | - | Callback when a file upload fails |
| onValidationError | (errors: ValidationError[]) => void | - | Callback for validation errors |
Upload Options
interface UploadOptions {
url: string // Upload endpoint
method?: 'POST' | 'PUT' // HTTP method (default: POST)
headers?: Record<string, string> | (() => Promise<Record<string, string>>)
fieldName?: string // Form field name (default: 'file')
formData?: Record<string, string | Blob> // Additional form data
withCredentials?: boolean // Include cookies
timeout?: number // Request timeout in ms
}Return Value
interface UseDropupReturn {
files: DropupFile[] // Array of files
actions: DropupActions // Action methods
state: ComputedState // Computed state
getDropProps: () => DropZoneProps // Props for drop zone element
getInputProps: () => InputProps // Props for hidden input
openFileDialog: () => void // Programmatically open file dialog
}Actions
| Action | Description |
|--------|-------------|
| upload() | Upload all pending files |
| uploadFile(id) | Upload a specific file |
| cancel() | Cancel all uploads |
| cancelFile(id) | Cancel a specific file upload |
| remove(id) | Remove a file from the list |
| reset() | Reset all state |
| retry(id) | Retry a failed upload |
| retryAll() | Retry all failed uploads |
State
| Property | Type | Description |
|----------|------|-------------|
| isUploading | boolean | Whether any file is uploading |
| isDragActive | boolean | Whether files are being dragged over |
| isDragAccept | boolean | Whether dragged files are accepted |
| isDragReject | boolean | Whether dragged files are rejected |
| progress | number | Overall upload progress (0-100) |
| status | 'idle' \| 'uploading' \| 'complete' \| 'error' | Overall status |
| error | DropupError \| null | Last error |
| counts | StatusCounts | File counts by status |
Advanced Usage
Chunked Uploads
For large files, use chunked uploads:
import { useDropup, createChunkedUploader } from '@samithahansaka/dropup'
const chunkedUploader = createChunkedUploader({
url: '/api/upload',
chunkSize: 5 * 1024 * 1024, // 5MB chunks
parallelChunks: 3,
})
const { files, actions } = useDropup({
upload: chunkedUploader,
})tus Protocol (Resumable Uploads)
For resumable uploads using the tus protocol:
npm install tus-js-clientimport { useDropup } from '@samithahansaka/dropup'
import { useTusUploader } from '@samithahansaka/dropup/tus'
const tusUploader = useTusUploader({
endpoint: 'https://tusd.example.com/files/',
chunkSize: 5 * 1024 * 1024,
})
const { files, actions } = useDropup({
upload: tusUploader,
})Cloud Storage (S3, GCS, Azure)
AWS S3
import { useDropup } from '@samithahansaka/dropup'
import { createS3Uploader } from '@samithahansaka/dropup/cloud/s3'
const s3Uploader = createS3Uploader({
getPresignedUrl: async (file) => {
const response = await fetch('/api/s3-presign', {
method: 'POST',
body: JSON.stringify({ filename: file.name, contentType: file.type }),
})
return response.json()
},
})
const { files, actions } = useDropup({
upload: s3Uploader,
})Google Cloud Storage
import { createGCSUploader } from '@samithahansaka/dropup/cloud/gcs'
const gcsUploader = createGCSUploader({
getPresignedUrl: async (file) => {
const response = await fetch('/api/gcs-presign', {
method: 'POST',
body: JSON.stringify({ filename: file.name }),
})
return response.json()
},
})Azure Blob Storage
import { createAzureUploader } from '@samithahansaka/dropup/cloud/azure'
const azureUploader = createAzureUploader({
getPresignedUrl: async (file) => {
const response = await fetch('/api/azure-presign', {
method: 'POST',
body: JSON.stringify({ filename: file.name }),
})
return response.json()
},
})Image Processing
import { useDropup } from '@samithahansaka/dropup'
import { compressImage, generatePreview, fixImageOrientation } from '@samithahansaka/dropup/image'
const { files } = useDropup({
accept: 'image/*',
onFilesAdded: async (newFiles) => {
for (const file of newFiles) {
// Generate preview
const preview = await generatePreview(file.file, { maxWidth: 200 })
// Compress before upload
const compressed = await compressImage(file.file, {
maxWidth: 1920,
maxHeight: 1080,
quality: 0.8,
})
}
},
})Custom Validation
import { useDropup, commonRules } from '@samithahansaka/dropup'
const { files } = useDropup({
accept: 'image/*',
maxSize: 5 * 1024 * 1024,
customRules: [
commonRules.safeFileName,
commonRules.extensionMatchesMime,
{
name: 'minDimensions',
validate: async (file) => {
// Custom async validation
const img = await createImageBitmap(file)
if (img.width < 100 || img.height < 100) {
return 'Image must be at least 100x100 pixels'
}
return true
},
},
],
})React Native
import { useDropup } from '@samithahansaka/dropup/native'
import * as ImagePicker from 'expo-image-picker'
function NativeUploader() {
const { files, actions } = useDropup({
upload: { url: 'https://api.example.com/upload' },
})
const pickImage = async () => {
const result = await ImagePicker.launchImageLibraryAsync({
mediaTypes: ImagePicker.MediaTypeOptions.Images,
})
if (!result.canceled) {
actions.addFiles(result.assets.map(asset => ({
uri: asset.uri,
name: asset.fileName || 'image.jpg',
type: asset.mimeType || 'image/jpeg',
size: asset.fileSize || 0,
})))
}
}
return (
<View>
<Button title="Pick Image" onPress={pickImage} />
{files.map(file => (
<Text key={file.id}>{file.name} - {file.progress}%</Text>
))}
</View>
)
}File Object
interface DropupFile {
id: string // Unique identifier
name: string // File name
size: number // File size in bytes
type: string // MIME type
status: FileStatus // 'idle' | 'pending' | 'uploading' | 'paused' | 'complete' | 'error'
progress: number // Upload progress (0-100)
file: File // Original File object
preview?: string // Preview URL (for images)
uploadedUrl?: string // URL after upload
error?: DropupError // Error if failed
meta?: Record<string, unknown> // Custom metadata
}Bundle Size
| Entry Point | Size (gzipped) |
|-------------|----------------|
| dropup | ~10KB |
| dropup/tus | ~1.5KB |
| dropup/image | ~5KB |
| dropup/cloud/s3 | ~700B |
| dropup/cloud/gcs | ~650B |
| dropup/cloud/azure | ~700B |
Browser Support
- Chrome (latest)
- Firefox (latest)
- Safari (latest)
- Edge (latest)
- React Native (iOS & Android)
TypeScript
dropup is written in TypeScript and provides comprehensive type definitions:
import type {
DropupFile,
UseDropupOptions,
UseDropupReturn,
UploadOptions,
ValidationError,
DropupError,
} from '@samithahansaka/dropup'Contributing
Contributions are welcome! Please read our Contributing Guide for details.
License
MIT © Samitha Hansaka
Related
- tus-js-client - tus resumable upload protocol
- react-dropzone - Simple drag-and-drop
