Skip to content

Lightweight, headless React file upload library with drag-drop, progress tracking, chunked uploads, and cloud storage support (S3, GCS, Azure)

License

Notifications You must be signed in to change notification settings

samithahansaka/dropup

dropup

npm version npm downloads bundle size TypeScript License: MIT

A lightweight, headless React file upload library with progress tracking, chunked uploads, and cloud storage support.

Documentation | Live Demo

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/dropup

Quick 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-client
import { 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 Widanage

Related

About

Lightweight, headless React file upload library with drag-drop, progress tracking, chunked uploads, and cloud storage support (S3, GCS, Azure)

Resources

License

Code of conduct

Contributing

Security policy

Stars

Watchers

Forks

Packages

No packages published

Languages