patterntypescriptreactModerate
File upload handling — FormData with React and TypeScript
Viewed 0 times
file uploadFormDatamultipartXMLHttpRequest progressfile type validationContent-Type multipartupload progress
Problem
File uploads require sending multipart/form-data requests, tracking upload progress, validating file types and sizes client-side, and providing meaningful error feedback. JSON-based fetch calls cannot include binary file data.
Solution
Use FileList from the input, validate client-side, and send via FormData without a Content-Type header:
import { useRef, useState } from 'react';
const ACCEPTED_TYPES = ['image/jpeg', 'image/png', 'image/webp'];
const MAX_SIZE_MB = 5;
function FileUploadForm() {
const inputRef = useRef<HTMLInputElement>(null);
const [error, setError] = useState<string | null>(null);
const [uploading, setUploading] = useState(false);
const [progress, setProgress] = useState(0);
const handleUpload = async (e: React.FormEvent) => {
e.preventDefault();
const file = inputRef.current?.files?.[0];
if (!file) return;
// Client-side validation
if (!ACCEPTED_TYPES.includes(file.type)) {
setError('Only JPEG, PNG, and WebP images are allowed');
return;
}
if (file.size > MAX_SIZE_MB 1024 1024) {
setError(
return;
}
setError(null);
setUploading(true);
const formData = new FormData();
formData.append('file', file);
formData.append('description', 'User avatar');
// Use XMLHttpRequest for progress events
const xhr = new XMLHttpRequest();
xhr.upload.addEventListener('progress', (ev) => {
if (ev.lengthComputable) setProgress(Math.round((ev.loaded / ev.total) * 100));
});
xhr.open('POST', '/api/upload');
xhr.send(formData);
xhr.onload = () => setUploading(false);
};
return (
<form onSubmit={handleUpload}>
<input ref={inputRef} type="file" accept="image/jpeg,image/png,image/webp" />
{error && <p role="alert">{error}</p>}
{uploading && <progress value={progress} max={100}>{progress}%</progress>}
<button type="submit" disabled={uploading}>Upload</button>
</form>
);
}
import { useRef, useState } from 'react';
const ACCEPTED_TYPES = ['image/jpeg', 'image/png', 'image/webp'];
const MAX_SIZE_MB = 5;
function FileUploadForm() {
const inputRef = useRef<HTMLInputElement>(null);
const [error, setError] = useState<string | null>(null);
const [uploading, setUploading] = useState(false);
const [progress, setProgress] = useState(0);
const handleUpload = async (e: React.FormEvent) => {
e.preventDefault();
const file = inputRef.current?.files?.[0];
if (!file) return;
// Client-side validation
if (!ACCEPTED_TYPES.includes(file.type)) {
setError('Only JPEG, PNG, and WebP images are allowed');
return;
}
if (file.size > MAX_SIZE_MB 1024 1024) {
setError(
File must be smaller than ${MAX_SIZE_MB}MB);return;
}
setError(null);
setUploading(true);
const formData = new FormData();
formData.append('file', file);
formData.append('description', 'User avatar');
// Use XMLHttpRequest for progress events
const xhr = new XMLHttpRequest();
xhr.upload.addEventListener('progress', (ev) => {
if (ev.lengthComputable) setProgress(Math.round((ev.loaded / ev.total) * 100));
});
xhr.open('POST', '/api/upload');
xhr.send(formData);
xhr.onload = () => setUploading(false);
};
return (
<form onSubmit={handleUpload}>
<input ref={inputRef} type="file" accept="image/jpeg,image/png,image/webp" />
{error && <p role="alert">{error}</p>}
{uploading && <progress value={progress} max={100}>{progress}%</progress>}
<button type="submit" disabled={uploading}>Upload</button>
</form>
);
}
Why
When using FormData with fetch or XMLHttpRequest, omitting the Content-Type header lets the browser set it automatically with the correct multipart boundary. Setting it manually produces an invalid boundary and the server rejects the request.
Gotchas
- NEVER set Content-Type manually when sending FormData — the browser must set the multipart boundary
- fetch does not support upload progress events — use XMLHttpRequest for progress tracking
- Client-side validation is a UX feature, not a security measure — always validate file type and size on the server
- file.type is user-supplied and can be spoofed — server must validate the actual file magic bytes
Revisions (0)
No revisions yet.