Skip to main content

Best File Upload Libraries for React in 2026

·PkgPulse Team

TL;DR

React Dropzone for the UI component; UploadThing for full-stack upload management. React Dropzone (~5M weekly downloads) is the headless file drop zone — handles drag-and-drop, file validation, and preview. UploadThing (~300K, growing fast) is a full-stack upload service from the T3 stack team — handles S3 uploads, file type validation, size limits, and webhooks. Filepond (~500K) is the full-featured upload widget with chunking, resumable uploads, and plugins.

Key Takeaways

  • React Dropzone: ~5M weekly downloads — headless, composable, pairs with any backend
  • UploadThing: ~300K downloads — T3 stack, managed S3, type-safe Next.js integration
  • Filepond: ~500K downloads — full widget, resumable chunks, image preview/transform
  • react-dropzone + presigned S3 URLs — most flexible for custom backends
  • UploadThing — free tier: 2GB storage, $25/10GB

React Dropzone (Headless)

// React Dropzone — headless, bring your own UI
import { useDropzone } from 'react-dropzone';
import { useState, useCallback } from 'react';

interface UploadedFile {
  file: File;
  preview: string;
  status: 'pending' | 'uploading' | 'done' | 'error';
  progress: number;
}

function ImageUploader({ onUpload }: { onUpload: (urls: string[]) => void }) {
  const [files, setFiles] = useState<UploadedFile[]>([]);

  const onDrop = useCallback((acceptedFiles: File[]) => {
    const newFiles = acceptedFiles.map(file => ({
      file,
      preview: URL.createObjectURL(file),
      status: 'pending' as const,
      progress: 0,
    }));
    setFiles(prev => [...prev, ...newFiles]);
    uploadFiles(newFiles);
  }, []);

  const { getRootProps, getInputProps, isDragActive, isDragReject } = useDropzone({
    onDrop,
    accept: { 'image/*': ['.jpg', '.jpeg', '.png', '.webp', '.gif'] },
    maxSize: 10 * 1024 * 1024,  // 10MB
    maxFiles: 5,
    onDropRejected: (rejections) => {
      rejections.forEach(({ file, errors }) => {
        console.error(`${file.name}: ${errors.map(e => e.message).join(', ')}`);
      });
    },
  });

  const uploadFiles = async (filesToUpload: UploadedFile[]) => {
    for (const fileData of filesToUpload) {
      setFiles(prev => prev.map(f =>
        f.file === fileData.file ? { ...f, status: 'uploading' } : f
      ));

      // Get presigned URL from your API
      const { url, key } = await getPresignedUrl(fileData.file.name, fileData.file.type);

      // Upload directly to S3
      const xhr = new XMLHttpRequest();
      xhr.upload.onprogress = (e) => {
        const progress = Math.round((e.loaded / e.total) * 100);
        setFiles(prev => prev.map(f =>
          f.file === fileData.file ? { ...f, progress } : f
        ));
      };

      await new Promise<void>((resolve, reject) => {
        xhr.onload = () => {
          setFiles(prev => prev.map(f =>
            f.file === fileData.file ? { ...f, status: 'done', progress: 100 } : f
          ));
          resolve();
        };
        xhr.onerror = reject;
        xhr.open('PUT', url);
        xhr.setRequestHeader('Content-Type', fileData.file.type);
        xhr.send(fileData.file);
      });
    }
  };

  return (
    <div>
      <div
        {...getRootProps()}
        className={`border-2 border-dashed rounded-lg p-8 text-center cursor-pointer transition-colors ${
          isDragActive ? 'border-blue-500 bg-blue-50' :
          isDragReject ? 'border-red-500 bg-red-50' :
          'border-gray-300 hover:border-gray-400'
        }`}
      >
        <input {...getInputProps()} />
        {isDragActive ? (
          <p>Drop files here...</p>
        ) : (
          <p>Drag & drop images here, or click to select (max 5 files, 10MB each)</p>
        )}
      </div>

      {files.length > 0 && (
        <div className="mt-4 grid grid-cols-3 gap-4">
          {files.map((fileData, i) => (
            <div key={i} className="relative">
              <img src={fileData.preview} className="w-full h-32 object-cover rounded" />
              {fileData.status === 'uploading' && (
                <div className="absolute inset-0 bg-black/50 flex items-center justify-center rounded">
                  <span className="text-white text-sm">{fileData.progress}%</span>
                </div>
              )}
              {fileData.status === 'done' && (
                <div className="absolute top-2 right-2 bg-green-500 text-white rounded-full w-6 h-6 flex items-center justify-center"></div>
              )}
            </div>
          ))}
        </div>
      )}
    </div>
  );
}

UploadThing (Full-Stack)

// UploadThing — server: define upload routes
// app/api/uploadthing/core.ts
import { createUploadthing, type FileRouter } from 'uploadthing/next';
import { auth } from '@/auth';

const f = createUploadthing();

export const ourFileRouter = {
  // Route 1: Profile image (authenticated, max 4MB)
  profileImage: f({ image: { maxFileSize: '4MB', maxFileCount: 1 } })
    .middleware(async ({ req }) => {
      const session = await auth();
      if (!session) throw new Error('Unauthorized');
      return { userId: session.user.id };
    })
    .onUploadComplete(async ({ metadata, file }) => {
      await updateUserAvatar(metadata.userId, file.url);
      return { uploadedBy: metadata.userId };
    }),

  // Route 2: Document upload (authenticated, multiple files)
  documentUpload: f({
    pdf: { maxFileSize: '32MB' },
    image: { maxFileSize: '8MB' },
  })
    .input(z.object({ projectId: z.string() }))
    .middleware(async ({ req, input }) => {
      const session = await auth();
      if (!session) throw new Error('Unauthorized');
      return { userId: session.user.id, projectId: input.projectId };
    })
    .onUploadComplete(async ({ metadata, file }) => {
      await saveDocument(metadata.userId, metadata.projectId, file.url);
    }),
} satisfies FileRouter;

export type OurFileRouter = typeof ourFileRouter;
// UploadThing — server: route handler
// app/api/uploadthing/route.ts
import { createRouteHandler } from 'uploadthing/next';
import { ourFileRouter } from './core';

export const { GET, POST } = createRouteHandler({ router: ourFileRouter });
// UploadThing — client: pre-built React components
import { UploadButton, UploadDropzone } from '@uploadthing/react';
import type { OurFileRouter } from '@/app/api/uploadthing/core';

function ProfileImageUpload({ userId }) {
  return (
    <UploadButton<OurFileRouter, 'profileImage'>
      endpoint="profileImage"
      onClientUploadComplete={(res) => {
        console.log('Uploaded:', res);
        toast.success('Profile photo updated!');
      }}
      onUploadError={(error) => {
        toast.error(`Upload failed: ${error.message}`);
      }}
    />
  );
}

// Full drag-and-drop zone
function DocumentUploader({ projectId }) {
  return (
    <UploadDropzone<OurFileRouter, 'documentUpload'>
      endpoint="documentUpload"
      input={{ projectId }}
      onClientUploadComplete={(res) => {
        console.log('Files uploaded:', res.map(r => r.url));
      }}
    />
  );
}

When to Choose

ScenarioPick
Custom UI, full controlReact Dropzone
Next.js + need simple S3 integrationUploadThing
Resumable uploads (large files)Filepond with chunk plugin
Image transform before uploadFilepond
Self-hosted S3 (MinIO)React Dropzone + presigned URLs
Maximum customizationReact Dropzone
Managed service, no backend codeUploadThing

Compare file upload library package health on PkgPulse.

Comments

Stay Updated

Get the latest package insights, npm trends, and tooling tips delivered to your inbox.