File Input
File Input
Handle file selection and validation.
Basic File Input
// components/FileInput.jsx
function FileInput({ accept, onChange, multiple = false, children }) {
const inputRef = useRef(null);
const handleClick = () => {
inputRef.current?.click();
};
const handleChange = (e) => {
const files = Array.from(e.target.files);
onChange(files);
e.target.value = ''; // Reset for re-selection
};
return (
<div>
<input
ref={inputRef}
type="file"
accept={accept}
multiple={multiple}
onChange={handleChange}
style={{ display: 'none' }}
/>
<button onClick={handleClick}>
{children || 'Choose File'}
</button>
</div>
);
}
File Validation
// utils/fileValidation.js
const FILE_TYPES = {
image: ['image/jpeg', 'image/png', 'image/gif', 'image/webp'],
document: ['application/pdf', 'application/msword'],
video: ['video/mp4', 'video/webm'],
};
function validateFile(file, options = {}) {
const {
maxSize = 10 * 1024 * 1024, // 10MB
allowedTypes = FILE_TYPES.image,
maxFiles = 1,
} = options;
const errors = [];
if (file.size > maxSize) {
errors.push(`File size must be less than ${formatSize(maxSize)}`);
}
if (!allowedTypes.includes(file.type)) {
errors.push(`File type ${file.type} is not allowed`);
}
return {
isValid: errors.length === 0,
errors,
};
}
// Component usage
function ValidatedFileInput() {
const [files, setFiles] = useState([]);
const [errors, setErrors] = useState([]);
const handleFiles = (newFiles) => {
const validatedFiles = [];
const newErrors = [];
newFiles.forEach(file => {
const result = validateFile(file, {
maxSize: 5 * 1024 * 1024,
allowedTypes: FILE_TYPES.image,
});
if (result.isValid) {
validatedFiles.push(file);
} else {
newErrors.push({ file: file.name, errors: result.errors });
}
});
setFiles(prev => [...prev, ...validatedFiles]);
setErrors(newErrors);
};
return (
<div>
<FileInput
accept="image/*"
multiple
onChange={handleFiles}
>
Upload Images
</FileInput>
{errors.length > 0 && (
<div className="errors">
{errors.map((error, i) => (
<p key={i}>{error.file}: {error.errors.join(', ')}</p>
))}
</div>
)}
</div>
);
}
File Preview
function FilePreview({ file }) {
const [preview, setPreview] = useState(null);
useEffect(() => {
if (file.type.startsWith('image/')) {
const reader = new FileReader();
reader.onloadend = () => setPreview(reader.result);
reader.readAsDataURL(file);
}
}, [file]);
return (
<div className="file-preview">
{preview ? (
<img src={preview} alt={file.name} />
) : (
<div className="file-icon">
{file.type.includes('pdf') ? '📄' : '📁'}
</div>
)}
<span className="file-name">{file.name}</span>
<span className="file-size">{formatSize(file.size)}</span>
</div>
);
}
Drag and Drop
Drag and Drop
Create intuitive drag-and-drop upload interfaces.
Drag and Drop Hook
// hooks/useDragAndDrop.js
function useDragAndDrop({ onDrop, accept }) {
const [isDragActive, setIsDragActive] = useState(false);
const [dragCounter, setDragCounter] = useState(0);
const handleDragEnter = useCallback((e) => {
e.preventDefault();
e.stopPropagation();
setDragCounter(prev => prev + 1);
if (e.dataTransfer.items && e.dataTransfer.items.length > 0) {
setIsDragActive(true);
}
}, []);
const handleDragLeave = useCallback((e) => {
e.preventDefault();
e.stopPropagation();
setDragCounter(prev => prev - 1);
if (dragCounter === 0) {
setIsDragActive(false);
}
}, [dragCounter]);
const handleDragOver = useCallback((e) => {
e.preventDefault();
e.stopPropagation();
}, []);
const handleDrop = useCallback((e) => {
e.preventDefault();
e.stopPropagation();
setIsDragActive(false);
setDragCounter(0);
const files = Array.from(e.dataTransfer.files);
const filteredFiles = accept
? files.filter(f => accept.some(type => f.type.match(type)))
: files;
if (filteredFiles.length > 0) {
onDrop(filteredFiles);
}
}, [onDrop, accept]);
return {
isDragActive,
dragProps: {
onDragEnter: handleDragEnter,
onDragLeave: handleDragLeave,
onDragOver: handleDragOver,
onDrop: handleDrop,
},
};
}
Drop Zone Component
// components/DropZone.jsx
function DropZone({ onDrop, accept, children, className = '' }) {
const { isDragActive, dragProps } = useDragAndDrop({ onDrop, accept });
return (
<div
className={`drop-zone ${isDragActive ? 'active' : ''} ${className}`}
{...dragProps}
>
{isDragActive ? (
<div className="drop-message">
<span className="drop-icon">📥</span>
<p>Drop files here</p>
</div>
) : (
children || (
<div className="drop-prompt">
<span className="drop-icon">📁</span>
<p>Drag & drop files here or click to browse</p>
</div>
)
)}
</div>
);
}
// Usage
function ImageUploader() {
const [files, setFiles] = useState([]);
const handleDrop = (newFiles) => {
setFiles(prev => [...prev, ...newFiles]);
};
return (
<div>
<DropZone
onDrop={handleDrop}
accept={['image/jpeg', 'image/png', 'image/webp']}
className="uploader"
>
<div className="upload-content">
<h3>Upload Images</h3>
<p>Supports JPG, PNG, WebP up to 10MB</p>
<FileInput
accept="image/*"
multiple
onChange={(f) => setFiles(prev => [...prev, ...f])}
>
Browse Files
</FileInput>
</div>
</DropZone>
<div className="file-list">
{files.map((file, i) => (
<FilePreview key={i} file={file} />
))}
</div>
</div>
);
}
Styling
.drop-zone {
border: 2px dashed #ccc;
border-radius: 8px;
padding: 40px;
text-align: center;
transition: all 0.2s;
cursor: pointer;
}
.drop-zone.active {
border-color: #2196f3;
background-color: rgba(33, 150, 243, 0.1);
}
.drop-icon {
font-size: 48px;
margin-bottom: 16px;
}
Progress Tracking and Chunked Uploads
Progress Tracking and Chunked Uploads
Upload with Progress
// utils/upload.js
async function uploadWithProgress(url, file, onProgress) {
return new Promise((resolve, reject) => {
const xhr = new XMLHttpRequest();
xhr.upload.addEventListener('progress', (e) => {
if (e.lengthComputable) {
const percent = Math.round((e.loaded / e.total) * 100);
onProgress(percent);
}
});
xhr.addEventListener('load', () => {
if (xhr.status >= 200 && xhr.status < 300) {
resolve(JSON.parse(xhr.response));
} else {
reject(new Error(`Upload failed: ${xhr.status}`));
}
});
xhr.addEventListener('error', () => reject(new Error('Upload failed')));
xhr.addEventListener('abort', () => reject(new Error('Upload aborted')));
const formData = new FormData();
formData.append('file', file);
xhr.open('POST', url);
xhr.send(formData);
});
}
Upload Hook
// hooks/useFileUpload.js
function useFileUpload(url) {
const [uploads, setUploads] = useState([]);
const upload = useCallback(async (file) => {
const uploadId = Date.now() + Math.random();
setUploads(prev => [...prev, {
id: uploadId,
file,
progress: 0,
status: 'uploading',
}]);
try {
const result = await uploadWithProgress(url, file, (progress) => {
setUploads(prev => prev.map(u =>
u.id === uploadId ? { ...u, progress } : u
));
});
setUploads(prev => prev.map(u =>
u.id === uploadId ? { ...u, status: 'success', result } : u
));
return result;
} catch (error) {
setUploads(prev => prev.map(u =>
u.id === uploadId ? { ...u, status: 'error', error } : u
));
throw error;
}
}, [url]);
const uploadMultiple = useCallback(async (files) => {
return Promise.all(files.map(file => upload(file)));
}, [upload]);
return { uploads, upload, uploadMultiple };
}
Chunked Upload
// utils/chunkedUpload.js
const CHUNK_SIZE = 5 * 1024 * 1024; // 5MB chunks
async function chunkedUpload(url, file, onProgress) {
const totalChunks = Math.ceil(file.size / CHUNK_SIZE);
let uploadedChunks = 0;
for (let i = 0; i < totalChunks; i++) {
const start = i * CHUNK_SIZE;
const end = Math.min(start + CHUNK_SIZE, file.size);
const chunk = file.slice(start, end);
const formData = new FormData();
formData.append('chunk', chunk);
formData.append('chunkIndex', i);
formData.append('totalChunks', totalChunks);
formData.append('fileName', file.name);
await fetch(url, {
method: 'POST',
body: formData,
});
uploadedChunks++;
onProgress(Math.round((uploadedChunks / totalChunks) * 100));
}
// Finalize upload
const response = await fetch(`${url}/finalize`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ fileName: file.name, totalChunks }),
});
return response.json();
}
Resume Failed Uploads
async function resumableUpload(url, file, onProgress) {
const uploadId = await getOrCreateUploadId(file);
const uploadedChunks = await getUploadedChunks(uploadId);
const totalChunks = Math.ceil(file.size / CHUNK_SIZE);
for (let i = 0; i < totalChunks; i++) {
if (uploadedChunks.includes(i)) {
continue; // Skip already uploaded
}
const start = i * CHUNK_SIZE;
const end = Math.min(start + CHUNK_SIZE, file.size);
const chunk = file.slice(start, end);
await uploadChunk(uploadId, i, chunk);
onProgress(Math.round(((i + 1) / totalChunks) * 100));
}
return finalizeUpload(uploadId);
}
Practice Problems
Create a reusable React component implementing File Uploads. Include proper state management and accessibility.
Solution
// Production-ready component with:
// - Proper TypeScript types
// - Accessibility (ARIA)
// - Error boundaries
// - Loading states
// - Memoization where neededWrite unit and integration tests for File Uploads using React Testing Library.
Solution
// Test coverage:
// 1. Rendering tests
// 2. Interaction tests
// 3. Edge case tests
// 4. Accessibility testsOptimize File Uploads for performance. Consider memoization, code splitting, and bundle size.
Solution
// Optimization techniques:
// 1. React.memo / useMemo / useCallback
// 2. Code splitting with lazy()
// 3. Virtual scrolling for lists
// 4. Image lazy loading
// 5. Bundle analysisQuiz
1. Why validate files on the client before upload?
2. What is the benefit of chunked uploads?
3. Why use XMLHttpRequest instead of fetch for progress tracking?
4. What should drag-and-drop zones handle?
5. How should you reset the file input after selection?
Flashcards
Question
Why validate files on the client?
Click to reveal answer
Answer
To provide instant feedback, prevent invalid uploads, and reduce unnecessary server requests.
Question
What are the drag-and-drop events?
Click to reveal answer
Answer
dragenter, dragleave, dragover, and drop events need to be handled.
Question
What is chunked upload?
Click to reveal answer
Answer
Splitting large files into smaller chunks that can be uploaded individually and resumed.
Question
How do you track upload progress?
Click to reveal answer
Answer
Use XMLHttpRequest's upload progress event or implement chunked progress tracking.
Question
What is File Uploads?
Click to reveal answer
Answer
File Uploads is a key concept in frontend development.
Revision Notes
Key Takeaways
- 1.Always validate file type and size on the client first
- 2.Drag-and-drop requires handling all four drag events
- 3.Use XMLHttpRequest for upload progress tracking
- 4.Chunked uploads enable resuming failed uploads
- 5.Reset file input to allow re-selection of same files
Interview Tips
- •Explain how to implement drag-and-drop file uploads
- •Discuss chunked uploads and their benefits
- •Know how to track upload progress
Cheat Sheet
File Uploads Cheat Sheet
File Input
<input type="file" onChange={handleChange} />
Drag and Drop
onDragEnter={handleDragEnter}
onDragLeave={handleDragLeave}
onDragOver={handleDragOver}
onDrop={handleDrop}
Progress Tracking
xhr.upload.addEventListener('progress', (e) => {
const percent = (e.loaded / e.total) * 100;
});
Chunked Upload
- Split file into 5MB chunks
- Upload sequentially
- Track which chunks uploaded
- Resume from last successful chunk