A File Upload Manager is a crucial component in web applications that allows users to select one or more files from their local system and upload them to a server. It goes beyond a simple HTML <input type="file"> by providing an enhanced user experience and robust handling of the upload process.
Key functionalities of a comprehensive File Upload Manager include:
1. File Selection: Users can select files either by clicking a button that opens a file browser dialog or by dragging and dropping files directly onto a designated area (drop zone).
2. File Previews: For certain file types (like images, videos, or PDFs), the manager can display a thumbnail or a basic preview of the file before it's uploaded. This helps users confirm they've selected the correct files.
3. Validation: Before upload, files can be validated against various criteria such as file type (MIME type), size limits, number of files, and dimensions (for images).
4. Upload Progress Tracking: Displays real-time progress for each file being uploaded, often with a progress bar and percentage complete. This provides feedback to the user and indicates that the application is active.
5. Cancellation/Removal: Users should be able to cancel an ongoing upload or remove a file from the selection queue before or during the upload process.
6. Error Handling: Gracefully handles various upload errors, such as network issues, server-side validation failures (e.g., file too large, incorrect format), or authentication problems. It should inform the user about the error.
7. Batch Uploads: Supports uploading multiple files simultaneously or in a queued manner.
8. Server Interaction: Communicates with a backend API (e.g., using AJAX, Fetch API) to send the file data and receive responses regarding the upload status. This often involves sending `FormData` objects.
9. User Interface (UI): Provides a clear and intuitive UI to display selected files, their status, progress, and actions (upload, remove, retry).
Why use a File Upload Manager?
* Improved User Experience: Provides visual feedback, allows previews, and makes the process feel more interactive and less like a black box.
* Robustness: Handles edge cases, errors, and provides retry mechanisms.
* Flexibility: Easily extendable to add more features like image manipulation, tagging, or advanced validation.
* Modern Web Standards: Leverages browser APIs like `FileReader`, `DragEvent`, and `XMLHttpRequest.upload.onprogress` to deliver a rich experience.
Building a File Upload Manager from scratch in React typically involves managing file state using `useState`, handling file input and drag-and-drop events, making asynchronous API calls for uploads, and updating the UI based on upload progress and status.
Example Code
```jsx
import React, { useState, useRef, useEffect } from 'react';
import './FileUploadManager.css'; // Assume you have some basic CSS for styling
const FileUploadManager = () => {
const [selectedFiles, setSelectedFiles] = useState([]);
const [uploadProgress, setUploadProgress] = useState({}); // { fileName: percentage }
const [uploadStatus, setUploadStatus] = useState({}); // { fileName: 'pending' | 'uploading' | 'completed' | 'failed' }
const fileInputRef = useRef(null);
const handleFileChange = (event) => {
const files = Array.from(event.target.files);
addFiles(files);
};
const handleDrop = (event) => {
event.preventDefault();
const files = Array.from(event.dataTransfer.files);
addFiles(files);
};
const addFiles = (files) => {
const newFiles = files.filter(file =>
!selectedFiles.some(existingFile => existingFile.name === file.name && existingFile.size === file.size)
);
setSelectedFiles(prevFiles => [...prevFiles, ...newFiles]);
newFiles.forEach(file => {
setUploadProgress(prev => ({ ...prev, [file.name]: 0 }));
setUploadStatus(prev => ({ ...prev, [file.name]: 'pending' }));
});
};
const handleRemoveFile = (fileName) => {
setSelectedFiles(prevFiles => prevFiles.filter(file => file.name !== fileName));
setUploadProgress(prev => {
const newState = { ...prev };
delete newState[fileName];
return newState;
});
setUploadStatus(prev => {
const newState = { ...prev };
delete newState[fileName];
return newState;
});
};
const simulateUpload = async (file) => {
setUploadStatus(prev => ({ ...prev, [file.name]: 'uploading' }));
console.log(`Starting upload for ${file.name}`);
const totalSize = file.size; // Or just simulate percentage out of 100
let uploaded = 0;
const chunkSize = Math.max(1, totalSize / 10); // Simulate 10 chunks
try {
while (uploaded < totalSize) {
await new Promise(resolve => setTimeout(resolve, 300)); // Simulate network latency
uploaded += chunkSize;
const currentProgress = Math.min(100, Math.round((uploaded / totalSize) * 100));
setUploadProgress(prev => ({ ...prev, [file.name]: currentProgress }));
// Simulate a failure for specific files or randomly
if (file.name.includes('fail') && currentProgress > 50) {
throw new Error('Simulated network error or server rejection');
}
if (currentProgress === 100) {
uploaded = totalSize; // Ensure it reaches 100%
}
}
setUploadStatus(prev => ({ ...prev, [file.name]: 'completed' }));
console.log(`Upload completed for ${file.name}`);
} catch (error) {
setUploadStatus(prev => ({ ...prev, [file.name]: 'failed' }));
console.error(`Upload failed for ${file.name}:`, error.message);
}
};
const handleUploadAll = async () => {
const filesToUpload = selectedFiles.filter(file => uploadStatus[file.name] === 'pending' || uploadStatus[file.name] === 'failed');
if (filesToUpload.length === 0) return;
// In a real app, you'd send each file to your backend API
// Example: Using FormData and Fetch API
// const formData = new FormData();
// formData.append('file', file);
// await fetch('/api/upload', { method: 'POST', body: formData });
// Simulate sequential uploads (can be parallelized too)
for (const file of filesToUpload) {
await simulateUpload(file);
}
};
const getFilePreview = (file) => {
if (file.type.startsWith('image/')) {
return URL.createObjectURL(file);
} else if (file.type.startsWith('video/')) {
return '<video width="100" controls><source src="' + URL.createObjectURL(file) + '" type="' + file.type + '"></video>';
}
return null;
};
useEffect(() => {
// Clean up created object URLs when files are removed or component unmounts
return () => {
selectedFiles.forEach(file => {
if (file.type.startsWith('image/') || file.type.startsWith('video/')) {
URL.revokeObjectURL(file.preview);
}
});
};
}, [selectedFiles]);
const isUploading = Object.values(uploadStatus).some(status => status === 'uploading');
const hasPendingFiles = selectedFiles.some(file => uploadStatus[file.name] === 'pending' || uploadStatus[file.name] === 'failed');
return (
<div className="file-upload-manager-container">
<h2>File Upload Manager</h2>
<div
className="drop-zone"
onDragOver={(event) => event.preventDefault()}
onDrop={handleDrop}
onClick={() => fileInputRef.current.click()}
>
<p>Drag & Drop files here or click to browse</p>
<input
type="file"
ref={fileInputRef}
multiple
hidden
onChange={handleFileChange}
accept=".jpg,.jpeg,.png,.gif,.pdf,.doc,.docx,.mp4,.mov"
/>
</div>
{selectedFiles.length > 0 && (
<div className="file-list-section">
<h3>Selected Files ({selectedFiles.length})</h3>
<ul className="file-list">
{selectedFiles.map((file) => (
<li key={file.name} className="file-item">
<div className="file-info">
{getFilePreview(file) && file.type.startsWith('image/') && (
<img src={getFilePreview(file)} alt="preview" className="file-preview-thumbnail" />
)}
{getFilePreview(file) && file.type.startsWith('video/') && (
<div className="file-preview-thumbnail" dangerouslySetInnerHTML={{ __html: getFilePreview(file) }} />
)}
<div className="file-details">
<span className="file-name">{file.name}</span>
<span className="file-size">({(file.size / 1024 / 1024).toFixed(2)} MB)</span>
</div>
</div>
<div className="file-status-section">
{uploadStatus[file.name] === 'uploading' && (
<div className="progress-bar-container">
<div
className="progress-bar"
style={{ width: `${uploadProgress[file.name] || 0}%` }}
></div>
<span className="progress-text">{uploadProgress[file.name] || 0}%</span>
</div>
)}
<span className={`file-status status-${uploadStatus[file.name]}`}>
{uploadStatus[file.name] === 'pending' && 'Ready to upload'}
{uploadStatus[file.name] === 'uploading' && 'Uploading...'}
{uploadStatus[file.name] === 'completed' && 'Uploaded'}
{uploadStatus[file.name] === 'failed' && 'Failed'}
</span>
<button
className="remove-button"
onClick={() => handleRemoveFile(file.name)}
disabled={uploadStatus[file.name] === 'uploading'}
>
×
</button>
</div>
</li>
))}
</ul>
<button
className="upload-all-button"
onClick={handleUploadAll}
disabled={isUploading || !hasPendingFiles}
>
{isUploading ? 'Uploading...' : 'Upload All Files'}
</button>
</div>
)}
</div>
);
};
export default FileUploadManager;
```
```css
/* FileUploadManager.css */
.file-upload-manager-container {
font-family: Arial, sans-serif;
max-width: 800px;
margin: 40px auto;
padding: 20px;
border: 1px solid #ddd;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
background-color: #fff;
}
h2 {
text-align: center;
color: #333;
margin-bottom: 25px;
}
.drop-zone {
border: 2px dashed #007bff;
border-radius: 5px;
padding: 40px 20px;
text-align: center;
cursor: pointer;
color: #007bff;
background-color: #eaf5ff;
margin-bottom: 25px;
transition: background-color 0.3s ease;
}
.drop-zone:hover {
background-color: #d9ecff;
}
.drop-zone p {
margin: 0;
font-size: 1.1em;
font-weight: 500;
}
.file-list-section h3 {
color: #333;
margin-top: 20px;
margin-bottom: 15px;
border-bottom: 1px solid #eee;
padding-bottom: 10px;
}
.file-list {
list-style: none;
padding: 0;
margin: 0;
}
.file-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 12px 0;
border-bottom: 1px solid #eee;
}
.file-item:last-child {
border-bottom: none;
}
.file-info {
display: flex;
align-items: center;
flex-grow: 1;
}
.file-preview-thumbnail {
width: 50px;
height: 50px;
object-fit: cover;
border-radius: 4px;
margin-right: 15px;
border: 1px solid #eee;
flex-shrink: 0;
}
.file-preview-thumbnail video {
width: 100%;
height: 100%;
border-radius: 4px;
}
.file-details {
display: flex;
flex-direction: column;
}
.file-name {
font-weight: 600;
color: #333;
font-size: 0.95em;
word-break: break-all;
}
.file-size {
font-size: 0.85em;
color: #666;
}
.file-status-section {
display: flex;
align-items: center;
margin-left: 20px;
flex-shrink: 0;
}
.progress-bar-container {
width: 100px;
height: 8px;
background-color: #e0e0e0;
border-radius: 4px;
overflow: hidden;
position: relative;
margin-right: 10px;
}
.progress-bar {
height: 100%;
background-color: #28a745;
border-radius: 4px;
transition: width 0.3s ease-in-out;
}
.progress-text {
position: absolute;
top: -15px;
right: 0;
font-size: 0.7em;
color: #555;
}
.file-status {
font-size: 0.8em;
padding: 3px 8px;
border-radius: 4px;
margin-right: 10px;
white-space: nowrap;
}
.status-pending {
background-color: #f0f0f0;
color: #555;
}
.status-uploading {
background-color: #e0f7fa;
color: #007bff;
}
.status-completed {
background-color: #e6ffe6;
color: #28a745;
}
.status-failed {
background-color: #ffe6e6;
color: #dc3545;
}
.remove-button {
background-color: #dc3545;
color: white;
border: none;
border-radius: 50%;
width: 24px;
height: 24px;
font-size: 1em;
cursor: pointer;
display: flex;
justify-content: center;
align-items: center;
transition: background-color 0.2s ease;
flex-shrink: 0;
}
.remove-button:hover {
background-color: #c82333;
}
.remove-button:disabled {
background-color: #e0e0e0;
cursor: not-allowed;
}
.upload-all-button {
display: block;
width: 100%;
padding: 12px 20px;
background-color: #007bff;
color: white;
border: none;
border-radius: 5px;
font-size: 1.1em;
cursor: pointer;
margin-top: 25px;
transition: background-color 0.2s ease;
}
.upload-all-button:hover {
background-color: #0056b3;
}
.upload-all-button:disabled {
background-color: #a0c3e6;
cursor: not-allowed;
}
```








File Upload Manager