File Uploads
Patterns for handling file uploads in React applications.
Basic File Input
import { useForm } from 'react-hook-form';
interface FormData {
avatar: FileList;
}
function AvatarUpload() {
const { register, handleSubmit, formState: { errors } } = useForm<FormData>();
const onSubmit = async (data: FormData) => {
const file = data.avatar[0];
const formData = new FormData();
formData.append('avatar', file);
await fetch('/api/upload', {
method: 'POST',
body: formData,
});
};
return (
<form onSubmit={handleSubmit(onSubmit)}>
<input
type="file"
accept="image/*"
{...register('avatar', { required: 'Please select a file' })}
/>
{errors.avatar && <span>{errors.avatar.message}</span>}
<button type="submit">Upload</button>
</form>
);
}File Validation with Zod
import { z } from 'zod';
const MAX_FILE_SIZE = 5 * 1024 * 1024; // 5MB
const ACCEPTED_IMAGE_TYPES = ['image/jpeg', 'image/png', 'image/webp'];
const uploadSchema = z.object({
avatar: z
.custom<FileList>()
.refine((files) => files?.length === 1, 'Please select a file')
.refine(
(files) => files?.[0]?.size <= MAX_FILE_SIZE,
'File size must be less than 5MB'
)
.refine(
(files) => ACCEPTED_IMAGE_TYPES.includes(files?.[0]?.type),
'Only .jpg, .png, and .webp formats are supported'
),
});
// For multiple files
const multiUploadSchema = z.object({
documents: z
.custom<FileList>()
.refine((files) => files?.length >= 1, 'Select at least one file')
.refine((files) => files?.length <= 5, 'Maximum 5 files allowed')
.refine(
(files) => Array.from(files || []).every((f) => f.size <= MAX_FILE_SIZE),
'Each file must be less than 5MB'
),
});Drag and Drop Upload
import { useState, useCallback } from 'react';
import { useDropzone } from 'react-dropzone';
interface FileWithPreview extends File {
preview: string;
}
function DragDropUpload() {
const [files, setFiles] = useState<FileWithPreview[]>([]);
const onDrop = useCallback((acceptedFiles: File[]) => {
setFiles(
acceptedFiles.map((file) =>
Object.assign(file, {
preview: URL.createObjectURL(file),
})
)
);
}, []);
const { getRootProps, getInputProps, isDragActive } = useDropzone({
onDrop,
accept: {
'image/*': ['.jpeg', '.jpg', '.png', '.webp'],
},
maxSize: 5 * 1024 * 1024,
maxFiles: 5,
});
// Cleanup previews on unmount
useEffect(() => {
return () => files.forEach((file) => URL.revokeObjectURL(file.preview));
}, [files]);
return (
<div>
<div
{...getRootProps()}
className={`
border-2 border-dashed rounded-lg p-8 text-center cursor-pointer
${isDragActive ? 'border-blue-500 bg-blue-50' : 'border-gray-300'}
`}
>
<input {...getInputProps()} />
{isDragActive ? (
<p>Drop the files here...</p>
) : (
<p>Drag & drop files here, or click to select</p>
)}
</div>
{/* Preview */}
<div className="grid grid-cols-3 gap-4 mt-4">
{files.map((file) => (
<div key={file.name} className="relative">
<img
src={file.preview}
alt={file.name}
className="w-full h-32 object-cover rounded"
/>
<button
onClick={() => setFiles(files.filter((f) => f !== file))}
className="absolute top-1 right-1 bg-red-500 text-white rounded-full p-1"
>
×
</button>
</div>
))}
</div>
</div>
);
}Upload with Progress
import { useState } from 'react';
import axios from 'axios';
function UploadWithProgress() {
const [progress, setProgress] = useState(0);
const [uploading, setUploading] = useState(false);
const handleUpload = async (file: File) => {
const formData = new FormData();
formData.append('file', file);
setUploading(true);
setProgress(0);
try {
await axios.post('/api/upload', formData, {
onUploadProgress: (progressEvent) => {
const percent = Math.round(
(progressEvent.loaded * 100) / (progressEvent.total || 1)
);
setProgress(percent);
},
});
} finally {
setUploading(false);
}
};
return (
<div>
<input
type="file"
onChange={(e) => e.target.files?.[0] && handleUpload(e.target.files[0])}
disabled={uploading}
/>
{uploading && (
<div className="mt-4">
<div className="bg-gray-200 rounded-full h-2">
<div
className="bg-blue-600 h-2 rounded-full transition-all"
style={{ width: `${progress}%` }}
/>
</div>
<p className="text-sm mt-1">{progress}% uploaded</p>
</div>
)}
</div>
);
}Image Preview Before Upload
import { useState } from 'react';
function ImagePreviewUpload() {
const [preview, setPreview] = useState<string | null>(null);
const { register, handleSubmit } = useForm<FormData>();
const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const file = e.target.files?.[0];
if (file) {
const reader = new FileReader();
reader.onloadend = () => {
setPreview(reader.result as string);
};
reader.readAsDataURL(file);
}
};
return (
<form onSubmit={handleSubmit(onSubmit)}>
<div className="flex items-center gap-4">
{preview ? (
<img
src={preview}
alt="Preview"
className="w-24 h-24 rounded-full object-cover"
/>
) : (
<div className="w-24 h-24 rounded-full bg-gray-200 flex items-center justify-center">
<span className="text-gray-500">No image</span>
</div>
)}
<label className="cursor-pointer bg-blue-500 text-white px-4 py-2 rounded">
Choose Image
<input
type="file"
accept="image/*"
className="hidden"
{...register('avatar')}
onChange={(e) => {
register('avatar').onChange(e);
handleFileChange(e);
}}
/>
</label>
</div>
<button type="submit">Save</button>
</form>
);
}Multiple File Upload
function MultipleFileUpload() {
const [files, setFiles] = useState<File[]>([]);
const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
if (e.target.files) {
setFiles(Array.from(e.target.files));
}
};
const removeFile = (index: number) => {
setFiles(files.filter((_, i) => i !== index));
};
const uploadAll = async () => {
const formData = new FormData();
files.forEach((file) => {
formData.append('files', file);
});
await fetch('/api/upload-multiple', {
method: 'POST',
body: formData,
});
};
return (
<div>
<input
type="file"
multiple
onChange={handleFileChange}
accept=".pdf,.doc,.docx"
/>
{files.length > 0 && (
<ul className="mt-4 space-y-2">
{files.map((file, index) => (
<li key={index} className="flex items-center justify-between">
<span>{file.name}</span>
<span className="text-gray-500 text-sm">
{(file.size / 1024).toFixed(1)} KB
</span>
<button onClick={() => removeFile(index)}>Remove</button>
</li>
))}
</ul>
)}
<button onClick={uploadAll} disabled={files.length === 0}>
Upload {files.length} files
</button>
</div>
);
}Presigned URL Upload (S3/Cloud Storage)
async function uploadToS3(file: File) {
// 1. Get presigned URL from your backend
const { uploadUrl, fileUrl } = await fetch('/api/get-upload-url', {
method: 'POST',
body: JSON.stringify({
fileName: file.name,
fileType: file.type,
}),
}).then((r) => r.json());
// 2. Upload directly to S3
await fetch(uploadUrl, {
method: 'PUT',
body: file,
headers: {
'Content-Type': file.type,
},
});
// 3. Return the public URL
return fileUrl;
}
function S3Upload() {
const [uploading, setUploading] = useState(false);
const [imageUrl, setImageUrl] = useState<string | null>(null);
const handleUpload = async (e: React.ChangeEvent<HTMLInputElement>) => {
const file = e.target.files?.[0];
if (!file) return;
setUploading(true);
try {
const url = await uploadToS3(file);
setImageUrl(url);
} finally {
setUploading(false);
}
};
return (
<div>
<input type="file" onChange={handleUpload} disabled={uploading} />
{uploading && <p>Uploading...</p>}
{imageUrl && <img src={imageUrl} alt="Uploaded" />}
</div>
);
}Custom File Input Styling
function StyledFileInput() {
const [fileName, setFileName] = useState<string>('');
return (
<div>
<label
htmlFor="file-upload"
className="
inline-flex items-center gap-2 px-4 py-2
bg-white border border-gray-300 rounded-md
cursor-pointer hover:bg-gray-50
focus-within:ring-2 focus-within:ring-blue-500
"
>
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2}
d="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z"
/>
</svg>
<span>{fileName || 'Choose file'}</span>
<input
id="file-upload"
type="file"
className="sr-only"
onChange={(e) => setFileName(e.target.files?.[0]?.name || '')}
/>
</label>
</div>
);
}Best Practices
- Validate on client and server - Never trust client-side validation alone
- Limit file size - Prevent large uploads that could crash the server
- Check file types - Validate MIME types, not just extensions
- Show progress - Users need feedback for large uploads
- Handle errors gracefully - Network issues, file too large, wrong type
- Clean up previews - Revoke object URLs to prevent memory leaks
- Use presigned URLs - Upload directly to cloud storage for large files