Quick answer
Upload files in React in 3 steps:
- Create file input with
<input type="file" onChange={handleFileChange} /> - Store file in state:
const [file, setFile] = useState(null) - Send to server:
const formData = new FormData(); formData.append('file', file); fetch('/upload', { method: 'POST', body: formData })
That’s it. You now have a working file upload.
What is file upload in React?
File upload is the ability to let users select files from their computer and send them to your server. This is fundamental to any app that needs to handle user-generated content: profile pictures, documents, resume uploads, invoice uploads, media files, etc.
In React, file upload involves:
- Input handling — Getting the file from the
<input type="file">element - State management — Storing the file in React state (using
useState) - Validation — Checking file size, type, and other requirements
- Upload — Sending the file to a backend server
- Feedback — Showing progress, success, or error messages
Unlike traditional form submissions, React file uploads happen without full page reloads, keeping your single-page app experience smooth and responsive.
Prerequisites
- A React app running with Vite (from the React + VS Code setup guide)
- Understanding of React hooks (useState, useRef)
- A backend endpoint to receive files (Node/Express, PHP, Python, etc.)
- ~20 minutes
Method 1: Basic file upload (single file)
The simplest way to upload a file
jsx
import { useState } from 'react'
function FileUpload() {
const [file, setFile] = useState(null)
const [message, setMessage] = useState('')
const handleFileChange = (e) => {
setFile(e.target.files[0]) // Get first file selected
}
const handleUpload = async () => {
if (!file) {
setMessage('Please select a file')
return
}
const formData = new FormData()
formData.append('file', file)
try {
const response = await fetch('/api/upload', {
method: 'POST',
body: formData
})
if (response.ok) {
setMessage('File uploaded successfully!')
setFile(null)
} else {
setMessage('Upload failed. Try again.')
}
} catch (error) {
setMessage('Error: ' + error.message)
}
}
return (
<div>
<input
type="file"
onChange={handleFileChange}
accept=".jpg,.png,.pdf"
/>
{file && <p>Selected: {file.name}</p>}
<button onClick={handleUpload}>Upload File</button>
{message && <p>{message}</p>}
</div>
)
}
export default FileUpload
What’s happening:
<input type="file">— Lets users pick a fileonChange={handleFileChange}— Captures the selected fileFormData()— Converts file to form data (required for file uploads)fetch('/api/upload', { method: 'POST', body: formData })— Sends to backend
Key point: Always use FormData for file uploads. Regular JSON won’t work.
Method 2: Drag and drop file upload
More user-friendly: let users drag files directly
jsx
import { useState } from 'react'
function DragDropUpload() {
const [file, setFile] = useState(null)
const [isDragActive, setIsDragActive] = useState(false)
const [uploadStatus, setUploadStatus] = useState('')
const handleDrag = (e) => {
e.preventDefault()
e.stopPropagation()
setIsDragActive(e.type === 'dragenter' || e.type === 'dragover')
}
const handleDrop = (e) => {
e.preventDefault()
e.stopPropagation()
setIsDragActive(false)
const droppedFiles = e.dataTransfer.files
if (droppedFiles.length > 0) {
setFile(droppedFiles[0])
}
}
const handleUpload = async () => {
if (!file) return
const formData = new FormData()
formData.append('file', file)
setUploadStatus('Uploading...')
try {
const response = await fetch('/api/upload', {
method: 'POST',
body: formData
})
if (response.ok) {
setUploadStatus('✓ Upload complete!')
setFile(null)
} else {
setUploadStatus('✗ Upload failed')
}
} catch (error) {
setUploadStatus('✗ Error: ' + error.message)
}
}
return (
<div
onDragEnter={handleDrag}
onDragLeave={handleDrag}
onDragOver={handleDrag}
onDrop={handleDrop}
style={{
border: isDragActive ? '2px dashed blue' : '2px dashed gray',
padding: '2rem',
textAlign: 'center',
cursor: 'pointer'
}}
>
<p>Drag and drop your file here, or click to select</p>
<input
type="file"
onChange={(e) => setFile(e.target.files[0])}
style={{ display: 'none' }}
/>
{file && <p>Selected: {file.name}</p>}
<button onClick={handleUpload} disabled={!file}>
Upload
</button>
{uploadStatus && <p>{uploadStatus}</p>}
</div>
)
}
export default DragDropUpload
New concepts:
onDragEnter,onDragOver,onDragLeave— Track when file hovers over drop zonee.dataTransfer.files— Get dropped files- Visual feedback (blue border when dragging)
Method 3: Upload with progress bar
Show real-time upload progress to users
jsx
import { useState } from 'react'
function FileUploadWithProgress() {
const [file, setFile] = useState(null)
const [progress, setProgress] = useState(0)
const [uploading, setUploading] = useState(false)
const handleUpload = async () => {
if (!file) return
setUploading(true)
const formData = new FormData()
formData.append('file', file)
try {
const xhr = new XMLHttpRequest()
// Track upload progress
xhr.upload.addEventListener('progress', (e) => {
if (e.lengthComputable) {
const percentComplete = (e.loaded / e.total) * 100
setProgress(percentComplete)
}
})
xhr.addEventListener('load', () => {
setProgress(100)
setUploading(false)
setFile(null)
alert('Upload complete!')
})
xhr.addEventListener('error', () => {
setUploading(false)
alert('Upload failed')
})
xhr.open('POST', '/api/upload')
xhr.send(formData)
} catch (error) {
setUploading(false)
alert('Error: ' + error.message)
}
}
return (
<div>
<input
type="file"
onChange={(e) => {
setFile(e.target.files[0])
setProgress(0)
}}
/>
{file && <p>File: {file.name}</p>}
{uploading && (
<div style={{ marginTop: '1rem' }}>
<div style={{
width: '100%',
height: '20px',
backgroundColor: '#eee',
borderRadius: '5px',
overflow: 'hidden'
}}>
<div style={{
height: '100%',
width: `${progress}%`,
backgroundColor: '#4CAF50',
transition: 'width 0.3s'
}}></div>
</div>
<p>{Math.round(progress)}% uploaded</p>
</div>
)}
<button onClick={handleUpload} disabled={!file || uploading}>
{uploading ? 'Uploading...' : 'Upload'}
</button>
</div>
)
}
export default FileUploadWithProgress
Key: xhr.upload.addEventListener('progress', ...) tracks real-time upload progress.
Method 4: Validation before upload
Validate file type and size on the client
jsx
import { useState } from 'react'
function ValidatedFileUpload() {
const [file, setFile] = useState(null)
const [error, setError] = useState('')
const MAX_FILE_SIZE = 5 * 1024 * 1024 // 5MB
const ALLOWED_TYPES = ['image/jpeg', 'image/png', 'application/pdf']
const handleFileChange = (e) => {
const selectedFile = e.target.files[0]
setError('')
// Check if file exists
if (!selectedFile) {
setFile(null)
return
}
// Check file size
if (selectedFile.size > MAX_FILE_SIZE) {
setError(`File too large. Max size: 5MB. Your file: ${(selectedFile.size / 1024 / 1024).toFixed(2)}MB`)
setFile(null)
return
}
// Check file type
if (!ALLOWED_TYPES.includes(selectedFile.type)) {
setError(`Invalid file type. Allowed: JPEG, PNG, PDF. Your file: ${selectedFile.type}`)
setFile(null)
return
}
setFile(selectedFile)
}
const handleUpload = async () => {
if (!file) return
const formData = new FormData()
formData.append('file', file)
try {
const response = await fetch('/api/upload', {
method: 'POST',
body: formData
})
if (response.ok) {
alert('File uploaded successfully!')
setFile(null)
}
} catch (error) {
setError('Upload error: ' + error.message)
}
}
return (
<div>
<input
type="file"
onChange={handleFileChange}
accept=".jpg,.jpeg,.png,.pdf"
/>
{error && <p style={{ color: 'red' }}>{error}</p>}
{file && <p style={{ color: 'green' }}>✓ {file.name} ({(file.size / 1024).toFixed(2)}KB)</p>}
<button onClick={handleUpload} disabled={!file}>
Upload
</button>
</div>
)
}
export default ValidatedFileUpload
Validation checks:
- File size ≤ 5MB
- File type is JPEG, PNG, or PDF
- User-friendly error messages
Multiple file uploads
Let users upload multiple files at once
jsx
import { useState } from 'react'
function MultipleFileUpload() {
const [files, setFiles] = useState([])
const [uploadStatus, setUploadStatus] = useState('')
const handleFileChange = (e) => {
// Convert FileList to Array to keep all files
setFiles(Array.from(e.target.files))
}
const handleUpload = async () => {
if (files.length === 0) {
setUploadStatus('Please select at least one file')
return
}
const formData = new FormData()
// Add all files to form data
files.forEach((file, index) => {
formData.append(`file-${index}`, file)
})
setUploadStatus('Uploading ' + files.length + ' file(s)...')
try {
const response = await fetch('/api/upload-multiple', {
method: 'POST',
body: formData
})
if (response.ok) {
setUploadStatus('✓ All ' + files.length + ' files uploaded!')
setFiles([])
}
} catch (error) {
setUploadStatus('✗ Error: ' + error.message)
}
}
return (
<div>
<input
type="file"
multiple
onChange={handleFileChange}
/>
{files.length > 0 && (
<div>
<p>Selected {files.length} file(s):</p>
<ul>
{files.map((file, index) => (
<li key={index}>{file.name} ({(file.size / 1024).toFixed(2)}KB)</li>
))}
</ul>
</div>
)}
<button onClick={handleUpload} disabled={files.length === 0}>
Upload All
</button>
{uploadStatus && <p>{uploadStatus}</p>}
</div>
)
}
export default MultipleFileUpload
Key: multiple attribute lets users select multiple files. Loop through files array to add each to FormData.
Upload methods comparison
| Method | Pros | Cons | Best for |
|---|---|---|---|
Basic <input> | Simple, standard HTML | No visual feedback | Simple forms, single file |
| Drag & drop | User-friendly, modern UX | Requires event handling | Photo uploaders, file managers |
| Progress bar | Shows real-time feedback | Requires XMLHttpRequest | Large files, slow connections |
| React Dropzone lib | Feature-rich, pre-built | Extra dependency | Complex upload scenarios |
| React Hook Form | Integrates with forms | Learning curve | Multi-step forms |
Upload libraries comparison
| Library | Size | Features | Best for |
|---|---|---|---|
| None (vanilla) | 0KB | Basic upload only | Simple projects |
| react-dropzone | 8KB | Drag-drop, validation | Modern UX requirements |
| React Dropzone | 8KB | Drag-drop, accessibility | Accessible apps |
| React Fine Uploader | 20KB | Progress, chunked uploads | Large files |
| Uppy | 25KB | Dashboard UI, plugins | Advanced uploads |
Recommendation: For most projects, vanilla React (FormData + fetch) is enough. Use libraries only if you need drag-drop or advanced features.
Server-side file handling (Node/Express example)
Your backend needs to receive the file
javascript
// server.js (Express backend)
const express = require('express')
const multer = require('multer') // npm install multer
const app = express()
// Configure file storage
const storage = multer.diskStorage({
destination: 'uploads/',
filename: (req, file, cb) => {
// Sanitize filename to prevent security issues
const sanitized = file.originalname.replace(/[^a-zA-Z0-9.-]/g, '_')
cb(null, Date.now() + '-' + sanitized)
}
})
const upload = multer({
storage,
limits: { fileSize: 5 * 1024 * 1024 }, // 5MB
fileFilter: (req, file, cb) => {
const allowed = ['image/jpeg', 'image/png', 'application/pdf']
if (allowed.includes(file.mimetype)) {
cb(null, true)
} else {
cb(new Error('Invalid file type'))
}
}
})
// Endpoint to receive file
app.post('/api/upload', upload.single('file'), (req, res) => {
if (!req.file) {
return res.status(400).json({ error: 'No file uploaded' })
}
res.json({
success: true,
filename: req.file.filename,
size: req.file.size,
message: 'File uploaded successfully'
})
})
app.listen(3000, () => console.log('Server on port 3000'))
Key points:
- Use
multerlibrary to handle file uploads - Validate file type and size on server (never trust client)
- Sanitize filenames to prevent security attacks
- Store files in a dedicated
uploads/directory
Security best practices
Critical: Always validate on the server
jsx
// ✅ GOOD: Server validates even if client validation fails
// Client validation is for UX, not security
// ❌ BAD: Relying only on client validation
// Attackers can bypass client checks
// ✅ GOOD: Sanitize filenames
const safe = filename.replace(/[^a-zA-Z0-9.-]/g, '_')
// ❌ BAD: Storing user-provided names directly
fs.writeFile(userFilename, data) // Dangerous!
// ✅ GOOD: Check MIME type on server
if (!['image/jpeg', 'image/png'].includes(file.mimetype)) reject()
// ❌ BAD: Trusting file extension
if (file.originalname.endsWith('.jpg')) accept() // Can be faked
// ✅ GOOD: Separate uploads from web root
// Store in /var/uploads, not /var/www/uploads
// ❌ BAD: Making uploads directly executable
// Always treat uploaded files as untrusted
FAQ
Q: Can I upload files without a backend?
A: Not really. You can use services like Firebase Storage or AWS S3 with client-side code, but you still need a backend system. Pure client-side upload requires a server somewhere.
Q: What’s FormData and why do I need it?
A: FormData is a browser API that converts files + data into multipart/form-data format, which is required for file uploads. Regular JSON can’t handle binary files.
Q: How do I preview images before upload?
A: Use FileReader.readAsDataURL():
jsx
const reader = new FileReader()
reader.onload = (e) => setPreview(e.target.result)
reader.readAsDataURL(file)
Q: Can I limit file uploads by extension?
A: Use <input accept=".jpg,.png,.pdf">, but always validate on the server too. Extensions can be faked.
Q: How do I handle very large files (100MB+)?
A: Use chunked uploads (split file into 5-10MB chunks, upload separately, reassemble on server). Libraries like Uppy or Fine Uploader handle this automatically.
Q: Is it safe to validate on the client side only?
A: No. Always validate on the server. Client validation is only for user experience; attackers bypass it easily.
Q: Can I upload to a different domain?
A: Yes, but CORS must allow it. Your server needs Access-Control-Allow-Origin headers. React can’t change browser CORS rules, only servers can.
Q: How do I show upload progress for multiple files?
A: Track progress for each file separately or show overall progress:
jsx
const totalSize = files.reduce((sum, f) => sum + f.size, 0)
const uploadedSize = files.reduce((sum, f) => sum + f.uploaded, 0)
const percentComplete = (uploadedSize / totalSize) * 100
Q: What if the user closes the browser during upload?
A: The request cancels. You can use AbortController to handle cleanup:
jsx
const controller = new AbortController()
fetch(url, { signal: controller.signal })
// Close browser → request aborts
Q: Can I resume interrupted uploads?
A: Yes, using chunked uploads + resumable protocols (like tus.io). Requires backend support.
Q: How do I delete uploaded files?
A: Send a DELETE request with the filename:
jsx
await fetch(`/api/delete/${filename}`, { method: 'DELETE' })
Troubleshooting
“413 Payload Too Large”
Your server rejected the file because it’s too big. Increase the limit in your server config (multer: limits: { fileSize: 50 * 1024 * 1024 }).
“CORS error: Access-Control-Allow-Origin”
The server doesn’t allow cross-origin requests. Ask your backend to add CORS headers or upload to the same domain.
“FormData not sending file”
Make sure you’re using fetch with method: 'POST' and body: formData (not headers: { 'Content-Type': 'application/json' }).
“File appears to upload but isn’t saved”
Check the server logs. The file might be failing validation on the backend. Always log server-side errors.
Next steps
You now understand file uploads in React. Ready to handle forms better?
Go back to the React + VS Code setup guide and explore:
- Building complete forms with validation
- Understanding React routing for multi-page file managers
- Backend integration to actually process uploads
File uploads are often part of larger workflows. Combining them with routing lets you build file management dashboards. Combining with validation lets you build professional forms.
Conclusion
File uploads in React are straightforward once you understand the flow: input → state → FormData → fetch → server. The basics take 20 lines of code. Advanced features (drag-drop, progress, validation) add complexity only when needed.
The key: always validate on the server, never trust the client.
Last updated: January 2026. Information current as of React 19, Node 20 LTS, modern Fetch API.