npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2025 – Pkg Stats / Ryan Hefner

@astrify/react-s3-upload

v0.3.2

Published

React file upload system built for S3-compatible storage with shadcn/ui components

Downloads

151

Readme

@astrify/react-s3-upload

A flexible, composable React file upload system built for S3-compatible storage. Features include drag-and-drop, progress tracking, detailed error handling, duplicate detection, and shadcn/ui component integration.

Features

  • 🎯 S3-Compatible Storage - Works with AWS S3, DigitalOcean Spaces, Cloudflare R2, MinIO, and more
  • 📦 Batch Upload Processing - Request signed URLs for multiple files in a single API call
  • 🔒 Duplicate Detection - SHA-256 hashing for deduplication
  • 📊 Progress Tracking - Real-time upload progress for each file
  • 🎨 Composable Components - Mix and match UI components to build custom upload interfaces
  • 🚀 Concurrent Uploads - Automatic queue management with configurable concurrency
  • ♻️ Error Recovery - Built-in retry mechanism for failed uploads
  • 🎯 Type-Safe - Full TypeScript support with comprehensive type definitions
  • 🧩 shadcn/ui Components - Pre-built components available via shadcn CLI

Installation

npm install @astrify/react-s3-upload
# or
pnpm add @astrify/react-s3-upload
# or
yarn add @astrify/react-s3-upload

Peer Dependencies

This package requires React 17 or higher:

{
  "peerDependencies": {
    "react": ">=17",
    "react-dom": ">=17"
  }
}

Quick Start

1. Install UI components from shadcn registry

# Install upload components
npx shadcn@latest add https://astrify.github.io/react-s3-upload/r/upload.json

### 2. Compose your own interface with individual components

```tsx
import { FileUploadProvider } from '@astrify/react-s3-upload';
import { Dropzone, List, Errors } from '@/components/astrify/upload';

function UploadSection() {
  return (
    <FileUploadProvider
      config={{
        signedUrlEndpoint: '/upload/signed-url',
        maxFiles: 10,
        maxSize: 50 * 1024 * 1024, // 50MB
        accept: 'image/*,application/pdf'
      }}
    >
      <div className="space-y-4">
        <Dropzone />
        <List />
        <Errors />
      </div>
    </FileUploadProvider>
  );
}

3. Use in a form (example)

import { useState } from 'react';
import { FileUploadProvider, useFileUpload } from '@astrify/react-s3-upload';
import { Dropzone, List, Errors } from '@/components/astrify/upload';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import { Button } from '@/components/ui/button';

// Main form component with the provider
function UploadForm() {
  return (
    <FileUploadProvider 
      config={{
        signedUrlEndpoint: '/upload/signed-url',
        maxFiles: 5,
        maxSize: 10 * 1024 * 1024, // 10MB
        accept: 'image/*,application/pdf'
      }}
    >
      <FormContent />
    </FileUploadProvider>
  );
}

function FormContent() {
  const { files, hasComplete, hasPending, hasUploading, hasErrors } = useFileUpload();

  const handleSubmit = (e: React.FormEvent) => {
    e.preventDefault();
    
    // Extract only completed files for submission
    const completedFiles = files.filter(f => f.status === 'complete');
    
    // Get form data
    const formData = new FormData(e.target as HTMLFormElement);
    
    // Submit with completed file data
    const submission = {
      name: formData.get('name'),
      files: completedFiles.map(f => ({
        id: f.id,
        name: f.name,
        url: f.url,
        sha256: f.sha256
      }))
    };
    
    console.log('Form submitted:', submission);
    // Send to your API here
  };

  // Enable submit only when all uploads are complete
  const canSubmit = hasComplete && !hasPending && !hasUploading && !hasErrors;

  return (
    <form onSubmit={handleSubmit} className="space-y-6">
      <div className="space-y-2">
        <Label htmlFor="name">Name</Label>
        <Input
          type="text"
          id="name"
          name="name"
          placeholder="Enter your name"
          required
        />
      </div>

      <div className="space-y-2">
        <Label>Attachments</Label>
        <div className="space-y-4">
          <Errors />
          <Dropzone />
          <List />
        </div>
      </div>

      <Button 
        type="submit"
        disabled={!canSubmit}
        className="w-full sm:w-auto"
      >
        Submit with {files.filter(f => f.status === 'complete').length} files
      </Button>
    </form>
  );
}

Server Integration

Request & Response Payloads

The uploader issues a POST to your presign endpoint (default /upload/signed-url) with JSON shaped like:

{
  "files": [
    {
      "filename": "invoice.pdf",
      "filesize": 58211,
      "contentType": "application/pdf",
      "sha256": "3f0d2f8c8d0d2b36f9b8c5c2f5deda4d3b1c7a6d1e9f5e8c6a7b8c9d0e1f2a3"
    }
  ]
}

Reply with a 200 JSON body describing each generated upload target. At minimum, return the matching sha256 plus the presigned URL details:

{
  "files": [
    {
      "sha256": "3f0d2f8c8d0d2b36f9b8c5c2f5deda4d3b1c7a6d1e9f5e8c6a7b8c9d0e1f2a3",
      "bucket": "my-uploads",
      "key": "uploads/9d1fcdcb-0f1f-4c82-bfd5-e8a4c5d9e123.pdf",
      "url": "https://my-uploads.s3.amazonaws.com/uploads/9d1fcdc...",
      "filename": "invoice.pdf"
    }
  ]
}

For validation failures return 422 Unprocessable Entity with an errors object (for example {"errors": {"files.0.filesize": ["File too large"]}}); the uploader surfaces those messages to the user.

Laravel Example

The package expects a server endpoint that returns presigned URLs for S3 uploads:

// routes/api.php
Route::post('/upload/signed-url', function (Request $request) {
    $validated = $request->validate([
        'files' => 'required|array',
        'files.*.filename' => 'required|string',
        'files.*.content_type' => 'required|string',
        'files.*.filesize' => 'required|integer',
        'files.*.sha256' => 'required|string',
    ]);
    
    $responses = [];
    
    foreach ($validated['files'] as $file) {
        // Check for duplicates
        if (File::where('sha256', $file['sha256'])->exists()) {
            return response()->json([
                'error' => 'Duplicate file detected'
            ], 422);
        }
        
        // Generate presigned URL
        $key = 'uploads/' . Str::uuid() . '.' . $file['extension'];
        $url = Storage::disk('s3')->temporaryUploadUrl(
            $key,
            now()->addMinutes(30),
            ['ContentType' => $file['content_type']]
        );
        
        $responses[] = [
            'sha256' => $file['sha256'],
            'bucket' => config('filesystems.disks.s3.bucket'),
            'key' => $key,
            'url' => $url,
            'filename' => $file['filename']
        ];
    }
    
    return response()->json(['files' => $responses]);
});

Node.js/Express Example

import { S3Client, PutObjectCommand } from '@aws-sdk/client-s3';
import { getSignedUrl } from '@aws-sdk/s3-request-presigner';

app.post('/upload/signed-url', async (req, res) => {
  const { files } = req.body;
  
  const responses = await Promise.all(files.map(async (file) => {
    const key = `uploads/${uuid()}.${file.extension}`;
    
    const command = new PutObjectCommand({
      Bucket: process.env.S3_BUCKET,
      Key: key,
      ContentType: file.content_type,
    });
    
    const url = await getSignedUrl(s3Client, command, {
      expiresIn: 1800, // 30 minutes
    });
    
    return {
      sha256: file.sha256,
      bucket: process.env.S3_BUCKET,
      key,
      url,
      filename: file.filename
    };
  }));
  
  res.json({ files: responses });
});

API Reference

FileUploadProvider

The main context provider that manages upload state and logic.

interface FileUploadConfig {
  maxFiles?: number;              // Maximum number of files (default: 10)
  maxSize?: number;               // Maximum file size in bytes (default: 50MB)
  accept?: string;                // Accepted file types (default: '*')
  multiple?: boolean;             // Allow multiple file selection (default: true)
  signedUrlEndpoint?: string;       // Endpoint for signed URL generation (default: '/upload/signed-url')
  presignHeaders?: Record<string, string> | (() => Record<string, string> | Promise<Record<string, string>>); // Optional headers for presign requests
  onUploadComplete?: (files: FileUpload[]) => void;
  onUploadError?: (errors: Array<{ file: File; error: any }>) => void;
  onFilesChange?: (files: File[]) => void;
}

useFileUpload Hook

Access the upload context and all functionality.

const {
  // State
  files,           // Current file collection
  errors,          // Error messages
  isUploading,     // Upload in progress
  remainingSlots,  // Available upload slots
  
  // Actions
  addFiles,        // Add files to upload
  removeFile,      // Remove a specific file
  removeAll,       // Clear all files
  retryUpload,     // Retry failed upload
  reset,           // Reset to initial state
  
  // Utilities
  canAcceptMore,   // Can accept more files
  acceptedFileTypes,
  maxFileSize
} = useFileUpload();

Types

interface FileUpload {
  id: string;              // SHA-256 hash
  name: string;            // File name
  size: number;            // File size in bytes
  type: string;            // MIME type
  sha256: string;          // SHA-256 hash
  url: string;             // Presigned upload URL
  status: UploadStatus;    // Upload status
  progress: number;        // Upload progress (0-100)
  error?: string;          // Error message if failed
  preview?: string;        // Preview URL for images
}

type UploadStatus = 'pending' | 'uploading' | 'complete' | 'error';

Contributing

We welcome contributions! Please see our Contributing Guide below for details on development setup and guidelines.

Development Setup

# Clone the repository
git clone https://github.com/astrify/react-s3-upload.git
cd react-s3-upload

# Install dependencies
pnpm install

# Start development mode
pnpm dev

Development Tools

  • tsup - TypeScript bundler for ESM and CJS outputs
  • Vite - Powers Storybook development
  • Vitest - Testing framework
  • Biome - Code formatting and linting
  • Lefthook - Git hooks for code quality
  • Commitizen - Standardized commit messages

Testing

Run tests with:

pnpm test

Tests are located in the tests/ directory and use Vitest with React Testing Library.

Building

Build the package with:

pnpm build

This creates ESM and CJS bundles in the dist/ directory.

🖇️ Linking

Often times you want to link this package to another project when developing locally, circumventing the need to publish to NPM to consume it. In a project where you want to consume your package run:

pnpm link @astrify/react-s3-upload --global

Learn more about package linking here.

Releasing

To create a new release:

pnpm release

This will:

  1. Build the package
  2. Create a git tag
  3. Generate a GitHub release
  4. Publish to npm (if configured)

Contributing Guide

  1. Fork the repository
  2. Create a feature branch (git checkout -b feature/amazing-feature)
  3. Make your changes
  4. Run tests (pnpm test)
  5. Run linting (pnpm lint)
  6. Commit your changes (pnpm commit)
  7. Push to your branch (git push origin feature/amazing-feature)
  8. Open a Pull Request

License

MIT © [Your Name]

Support

Acknowledgments

Built with: