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
| Scenario | Pick |
|---|---|
| Custom UI, full control | React Dropzone |
| Next.js + need simple S3 integration | UploadThing |
| Resumable uploads (large files) | Filepond with chunk plugin |
| Image transform before upload | Filepond |
| Self-hosted S3 (MinIO) | React Dropzone + presigned URLs |
| Maximum customization | React Dropzone |
| Managed service, no backend code | UploadThing |
Compare file upload library package health on PkgPulse.
See the live comparison
View uploadthing vs. react dropzone on PkgPulse →