This document presents a comprehensive low-level design for a robust file upload component that handles multiple file types, provides real-time progress tracking, and includes comprehensive error handling for enterprise-grade applications.
1. Problem Statement & Requirements
Clarifying questions
- What file types and size limits are expected for uploads? Are there any specific MIME type restrictions?
- Should this component support drag-and-drop functionality, or is a traditional file input sufficient?
- Is authentication required for file uploads, and what are the authorization requirements?
- Do we need to support real-time upload progress tracking or is a simple success/failure response enough?
- Should this component work in mobile browsers and support touch interactions?
- Are there any accessibility requirements beyond basic ARIA compliance (e.g., screen reader support, keyboard navigation)?
- Do we need to support internationalization for error messages and UI text?
Functional requirements
- Users can select files from their device using a file input dialog
- Users can drag and drop files onto the upload area (if supported)
- Upload progress tracking with visual indicators
- Ability to cancel ongoing uploads
- Support for multiple file selection (batch upload)
- File validation (type, size limits) with appropriate error messages
- Visual preview of supported file types (images, documents)
- Upload status indicators (pending, uploading, success, error)
- Ability to retry failed uploads
- Support for file metadata (name, size, type) display
Non-functional requirements
- Performance: Uploads should not block the UI and should maintain good responsiveness
- Accessibility: Full keyboard navigation support, ARIA roles for screen readers
- Scalability: Component should handle large numbers of files efficiently
- Reliability: Graceful handling of network failures and partial uploads
- Security: Input sanitization, file type validation, size limits to prevent abuse
Constraints and assumptions
- Target browsers: Chrome 80+, Firefox 70+, Safari 13+, Edge 80+
- Mobile support: Touch-friendly interactions and responsive design
- Authentication handled at the API layer, not within this component
- No server-side file processing or transformation required
- Component will be used in a React 18+ application with TypeScript
- No third-party drag-and-drop libraries will be used (custom implementation)
Out of scope
- File compression or conversion capabilities
- Server-side file processing or transformation
- Integration with cloud storage services (e.g., AWS S3, Google Cloud)
- File versioning or revision history
- Multi-part upload chunking for large files (would require backend support)
- Integration with file sharing services or external APIs
- File encryption or secure transfer protocols beyond standard HTTPS
- Custom file icon generation (rely on system/file type detection)
2. Component Architecture
graph TD
A[FileUpload] --> B[FileDropZone]
A --> C[FileList]
A --> D[FilePreview]
B --> E[FileInput]
C --> F[FileItem]
D --> G[ImagePreview]
D --> H[FileIcon]
classDef smart fill:#4CAF50,stroke:#333,color:white;
classDef dumb fill:#2196F3,stroke:#333,color:white;
class A,B,C,D,E,F,G,H smart
Component Tree and Responsibilities
Smart Components (Container Components):
FileUpload- Main container that orchestrates the entire upload flow, manages state, and handles API interactionsFileDropZone- Handles drag-and-drop functionality and file selection via clickFileList- Manages collection of selected files, handles removal and batch operations
Dumb Components (Presentational Components):
FileItem- Individual file display with status indicators and progressFilePreview- Renders preview for supported file types (images, documents)ImagePreview- Specialized image preview with zoom capabilityFileIcon- Generic file type icon rendering
TypeScript Props Contracts
// FileUpload component props
interface FileUploadProps {
onUploadComplete?: (files: UploadedFile[]) => void;
onUploadError?: (error: UploadError) => void;
maxFileSize?: number; // in bytes
allowedFileTypes?: string[];
maxFiles?: number;
multiple?: boolean;
accept?: string;
showPreview?: boolean;
uploadUrl: string;
}
// FileItem component props
interface FileItemProps {
file: UploadedFile;
onRemove?: (fileId: string) => void;
onRetry?: (fileId: string) => void;
showPreview?: boolean;
}
// FileDropZone component props
interface FileDropZoneProps {
onFilesSelected: (files: File[]) => void;
accept?: string;
multiple?: boolean;
disabled?: boolean;
children?: React.ReactNode;
}
UI State Machine
stateDiagram-v2
[*] --> Idle
Idle --> Dragging : dragenter
Dragging --> Idle : dragleave
Dragging --> Uploading : drop
Dragging --> Idle : cancel
Idle --> Selecting : click
Selecting --> Uploading : fileSelected
Uploading --> Success : uploadSuccess
Uploading --> Error : uploadError
Error --> Idle : reset
Success --> Idle : reset
Uploading --> Canceling : cancel
Canceling --> Idle : canceled
Component Responsibilities
- FileUpload: Main orchestrator, handles state management, API communication, and error handling
- FileDropZone: Implements drag-and-drop UX, file selection, and validation
- FileList: Manages collection of files with batch operations (remove all, retry all)
- FileItem: Individual file representation with status tracking and actions
- FilePreview: Renders previews for supported file types with fallback to generic icons
Reusability Decisions
The component is designed for maximum reusability:
- All presentational components are pure and props-driven
- Validation logic is abstracted into reusable hooks
- Themeable through CSS variables for easy customization
- Support for multiple file types and preview strategies
Component Lifecycle Considerations
- Mount: Initialize state, set up event listeners for drag-and-drop
- Update: Handle file selection changes, progress updates, and state transitions
- Unmount: Clean up event listeners, cancel ongoing uploads, clear timeouts
- Error Recovery: Support retry mechanisms and graceful degradation
The component lifecycle is designed to handle file selection, upload progress tracking, and cleanup operations efficiently while maintaining a clean separation between UI concerns and business logic.
3. State Management Design
Local state vs. global state decisions
For a file upload component, local state is appropriate for:
- Upload progress tracking (per-file)
- File validation states
- UI interaction states (dragging, uploading, success/failure)
- Temporary file metadata (name, size, type)
Global state is appropriate for:
- Upload queue management
- Overall upload status (pending, in-progress, completed)
- Shared configuration settings (max file size, allowed types)
State management approach
Zustand is selected as the state management solution because:
- Minimal boilerplate with simple API
- Excellent TypeScript support with type inference
- No provider wrapping needed - direct hook usage
- Easy to test and mock
- Good performance characteristics for this use case
State shape and normalization strategy
interface FileUploadState {
files: FileRecord[];
uploadQueue: UploadQueueItem[];
overallStatus: 'idle' | 'uploading' | 'completed' | 'error';
config: UploadConfig;
}
interface FileRecord {
id: string;
file: File;
status: 'pending' | 'validating' | 'queued' | 'uploading' | 'success' | 'error';
progress: number;
error?: string;
}
interface UploadQueueItem {
id: string;
fileId: string;
status: 'pending' | 'in-progress' | 'completed' | 'failed';
progress: number;
}
interface UploadConfig {
maxFileSize: number; // bytes
allowedTypes: string[];
maxConcurrentUploads: number;
}
Server state vs. client state separation
Client state:
- File metadata (name, size, type)
- Upload progress tracking
- UI states and validation errors
- Local file object references
Server state:
- Actual uploaded file IDs (from backend)
- File URLs and metadata from server
- Upload completion confirmation
Cache invalidation strategy
- When a file is successfully uploaded, remove it from client state
- On upload failure, retain file in queue with error status for retry
- When user clears all files, reset entire state
- On configuration changes (max file size, allowed types), invalidate and revalidate pending files
Optimistic updates design
- Show file as "uploaded" immediately in UI when added to queue
- Update progress bar in real-time during upload
- Show success/failure feedback immediately upon completion
- Rollback UI changes on server error
Critical-path hook implementation
import { create } from 'zustand';
import { immer } from 'zustand/middleware/immer';
interface FileUploadState {
files: FileRecord[];
uploadQueue: UploadQueueItem[];
overallStatus: 'idle' | 'uploading' | 'completed' | 'error';
config: UploadConfig;
addFiles: (files: File[]) => void;
removeFile: (fileId: string) => void;
clearAll: () => void;
setConfig: (config: Partial<UploadConfig>) => void;
startUpload: () => void;
cancelUpload: (fileId: string) => void;
}
interface FileRecord {
id: string;
file: File;
status: 'pending' | 'validating' | 'queued' | 'uploading' | 'success' | 'error';
progress: number;
error?: string;
}
interface UploadQueueItem {
id: string;
fileId: string;
status: 'pending' | 'in-progress' | 'completed' | 'failed';
progress: number;
}
interface UploadConfig {
maxFileSize: number;
allowedTypes: string[];
maxConcurrentUploads: number;
}
const useFileUploadStore = create<FileUploadState>()(
immer((set, get) => ({
files: [],
uploadQueue: [],
overallStatus: 'idle',
config: {
maxFileSize: 10 * 1024 * 1024, // 10MB default
allowedTypes: ['image/*', 'application/pdf'],
maxConcurrentUploads: 3,
},
addFiles: (files) => {
set((state) => {
const newFiles = files.map((file) => ({
id: Math.random().toString(36).substr(2, 9),
file,
status: 'pending' as const,
progress: 0,
}));
// Validate files before adding
const validatedFiles = newFiles.map((fileRecord) => {
if (fileRecord.file.size > state.config.maxFileSize) {
return {
...fileRecord,
status: 'error' as const,
error: 'File size exceeds maximum limit',
};
}
if (!state.config.allowedTypes.some((type) =>
type === '*' || fileRecord.file.type.startsWith(type.replace('*', '')))) {
return {
...fileRecord,
status: 'error' as const,
error: 'File type not allowed',
};
}
return {
...fileRecord,
status: 'validating' as const,
};
});
state.files.push(...validatedFiles);
});
},
removeFile: (fileId) => {
set((state) => {
const fileIndex = state.files.findIndex(f => f.id === fileId);
if (fileIndex !== -1) {
state.files.splice(fileIndex, 1);
}
const queueIndex = state.uploadQueue.findIndex(q => q.fileId === fileId);
if (queueIndex !== -1) {
state.uploadQueue.splice(queueIndex, 1);
}
// Update overall status if no files remain
if (state.files.length === 0 && state.uploadQueue.length === 0) {
state.overallStatus = 'idle';
}
});
},
clearAll: () => {
set({
files: [],
uploadQueue: [],
overallStatus: 'idle',
});
},
setConfig: (config) => {
set((state) => {
state.config = { ...state.config, ...config };
});
},
startUpload: () => {
set((state) => {
const pendingFiles = state.files.filter(f => f.status === 'validating');
// Move valid files to upload queue
const newQueueItems = pendingFiles.map((fileRecord) => ({
id: Math.random().toString(36).substr(2, 9),
fileId: fileRecord.id,
status: 'pending' as const,
progress: 0,
}));
state.uploadQueue.push(...newQueueItems);
state.overallStatus = 'uploading';
// Update file statuses
pendingFiles.forEach((fileRecord) => {
const fileIndex = state.files.findIndex(f => f.id === fileRecord.id);
if (fileIndex !== -1) {
state.files[fileIndex].status = 'queued';
}
});
});
},
cancelUpload: (fileId) => {
set((state) => {
const queueIndex = state.uploadQueue.findIndex(q => q.fileId === fileId);
if (queueIndex !== -1) {
state.uploadQueue.splice(queueIndex, 1);
}
const fileIndex = state.files.findIndex(f => f.id === fileId);
if (fileIndex !== -1) {
state.files[fileIndex].status = 'pending';
}
});
},
}))
);
// Custom hook for file upload operations
export const useFileUpload = () => {
const {
files,
uploadQueue,
overallStatus,
config,
addFiles,
removeFile,
clearAll,
setConfig,
startUpload,
cancelUpload,
} = useFileUploadStore();
return {
files,
uploadQueue,
overallStatus,
config,
addFiles,
removeFile,
clearAll,
setConfig,
startUpload,
cancelUpload,
};
};
4. Routing & Navigation
Route Structure and Hierarchy
The file upload feature will be integrated into an existing application's routing structure. The component will be accessible through a nested route under the /files path, with potential sub-routes for different file types or upload contexts.
graph TD
A[Root Route] --> B[/files]
B --> C[/upload]
B --> D[/browse]
B --> E[/recent]
- Public vs Protected: The file upload functionality will be protected behind authentication, accessible only to authenticated users.
- Nested Routes: The
/files/uploadroute will be a nested route under the main files section, allowing for consistent navigation and UI layout. - Layout Routes: A shared layout component will wrap the file upload feature, providing consistent header, sidebar navigation, and footer across all file-related views.
Code Splitting Strategy
Code splitting will be implemented at the route level to optimize initial bundle size:
- Route-based code splitting: The file upload component will be loaded as a separate chunk when the user navigates to
/files/upload. This ensures that heavy dependencies like file processing libraries are only loaded when needed. - Component-based code splitting: The drag-and-drop zone and preview components will be dynamically imported to reduce initial load time.
// Route configuration with dynamic imports
const routes = [
{
path: '/files',
element: <FilesLayout />,
children: [
{
index: true,
element: <Navigate to="/files/browse" />
},
{
path: 'upload',
element: React.lazy(() => import('./components/FileUploadPage')),
loader: authLoader
},
{
path: 'browse',
element: React.lazy(() => import('./components/FileBrowser')),
loader: authLoader
}
]
}
];
Navigation Guards and Authentication Flows
Authentication guards will be implemented to ensure that only authenticated users can access file upload functionality:
- Protected Route: The
/files/uploadroute will be protected using a route guard that checks for valid authentication tokens. - Redirect Logic: Unauthenticated users will be redirected to the login page with a return URL parameter.
- Session Management: The authentication guard will also handle session expiration and token refresh logic.
Deep Linking Support and URL Parameter Design
Deep linking will be supported for direct access to specific upload contexts:
- Query Parameters: The upload component will support query parameters for pre-filling upload context:
?type=image- Pre-selects image file type?folderId=123- Pre-selects a specific folder for upload?redirect=/dashboard- Specifies redirect after successful upload
// URL parameter parsing for deep linking
const parseUploadParams = (searchParams: URLSearchParams) => {
return {
fileType: searchParams.get('type') || 'all',
folderId: searchParams.get('folderId') || null,
redirectUrl: searchParams.get('redirect') || '/dashboard'
};
};
Browser History Management and Scroll Restoration
Browser history management will be handled through React Router's built-in scroll restoration:
- Scroll Restoration: The browser will automatically restore scroll position when users navigate back to the file upload page.
- History Management: The component will properly handle browser back/forward navigation without losing state or triggering unnecessary re-renders.
- State Preservation: Critical upload state (selected files, progress) will be preserved during navigation to prevent data loss.
// Scroll restoration configuration in React Router v6+
import { useScrollRestoration } from 'react-router-dom';
function FileUploadPage() {
useScrollRestoration();
return (
<div className="file-upload-container">
{/* Upload component */}
</div>
);
}
5. API Integration Layer
API Client Design Pattern
We'll implement a custom fetch wrapper with axios instance for consistent HTTP handling:
// api/client.ts
import axios, { AxiosInstance, AxiosRequestConfig } from 'axios';
const createApiClient = (): AxiosInstance => {
const client = axios.create({
baseURL: process.env.NEXT_PUBLIC_API_BASE_URL,
timeout: 30000,
headers: {
'Content-Type': 'application/json',
},
});
// Request interceptor for auth token injection
client.interceptors.request.use(
(config) => {
const token = localStorage.getItem('authToken');
if (token) {
config.headers.Authorization = `Bearer ${token}`;
}
return config;
},
(error) => Promise.reject(error)
);
// Response interceptor for error handling
client.interceptors.response.use(
(response) => response,
(error) => {
if (error.response?.status === 401) {
// Handle auth errors
localStorage.removeItem('authToken');
window.location.href = '/login';
}
return Promise.reject(error);
}
);
return client;
};
export const apiClient = createApiClient();
Request/Response Interceptors
Authentication token injection is handled via request interceptor, while response interceptor handles 401 errors by redirecting to login page. All responses are normalized to a consistent structure:
// api/types.ts
export interface ApiResponse<T> {
data: T;
status: number;
message?: string;
}
export interface UploadResponse {
id: string;
name: string;
size: number;
url: string;
uploadDate: string;
}
Error Handling and Retry Strategy
Implement exponential backoff retry logic for transient errors (429, 500, 503):
// api/uploadService.ts
class UploadService {
private maxRetries = 3;
private retryDelay = 1000;
async uploadFile(
file: File,
options?: { onProgress?: (progress: number) => void }
): Promise<UploadResponse> {
const formData = new FormData();
formData.append('file', file);
let lastError: Error;
for (let attempt = 0; attempt <= this.maxRetries; attempt++) {
try {
const response = await apiClient.post<UploadResponse>('/upload', formData, {
onUploadProgress: (progressEvent) => {
if (options?.onProgress && progressEvent.total) {
const progress = Math.round(
(progressEvent.loaded * 100) / progressEvent.total
);
options.onProgress(progress);
}
},
});
return response.data;
} catch (error) {
lastError = error as Error;
if (
attempt < this.maxRetries &&
(error.response?.status === 429 ||
error.response?.status === 500 ||
error.response?.status === 503)
) {
await this.delay(this.retryDelay * Math.pow(2, attempt));
continue;
}
throw error;
}
}
throw lastError;
}
private delay(ms: number): Promise<void> {
return new Promise(resolve => setTimeout(resolve, ms));
}
}
export const uploadService = new UploadService();
Loading, Error, and Empty States Design
All API interactions should handle loading states with progress indicators and provide clear error messaging:
// components/FileUpload.tsx
interface FileUploadState {
status: 'idle' | 'uploading' | 'success' | 'error';
progress: number;
error?: string;
uploadedFile?: UploadResponse;
}
Data Fetching Strategy
For file upload, we use client-side rendering (CSR) since uploads are immediate operations and don't benefit from SSR. The upload endpoint is designed to be stateless:
// pages/upload.tsx
import { GetServerSideProps } from 'next';
export const getServerSideProps: GetServerSideProps = async () => {
// Upload page is client-only, no server data needed
return {
props: {},
};
};
API Response Transformation Layer
All API responses are transformed to domain models for consistent usage:
// api/mappers.ts
import { UploadResponse } from './types';
export interface FileMetadata {
id: string;
name: string;
size: number;
url: string;
uploadDate: Date;
}
export const mapUploadResponseToMetadata = (
response: UploadResponse
): FileMetadata => ({
id: response.id,
name: response.name,
size: response.size,
url: response.url,
uploadDate: new Date(response.uploadDate),
});
Sequence Diagram for Primary Data Flows
sequenceDiagram
participant U as User
participant C as FileUploadComponent
participant A as UploadService
participant S as API Server
U->>C: Select file(s)
C->>A: Call uploadFile()
A->>S: POST /upload with FormData
S-->>A: Response with file metadata
A-->>C: Return file metadata
C->>U: Show success/error state
6. Performance Design
Core Web Vitals Targets
- Largest Contentful Paint (LCP): <2.5s for file upload UI elements
- Interaction to Next Paint (INP): <200ms for user interactions (drag/drop, click, progress updates)
- Cumulative Layout Shift (CLS): <0.1 for upload area and progress indicators
Bundle Size Budget per Route
- File Upload Component: <5KB (minified + gzipped)
- Drag-and-Drop Polyfill: <2KB (if needed)
- Image Preview Library: <1KB (if used)
- File Validation Utilities: <1KB
Critical Rendering Path Optimization
- Critical CSS: Inline minimal styles for upload area and progress indicators
- Non-critical JS: Defer file validation and preview logic until user interaction
- Render-blocking resources: All file upload logic deferred to prevent blocking main thread
Image Optimization Strategy
- Preview Images:
- Resize to max 800px width for preview thumbnails
- Use WebP format when supported, fallback to JPEG
- Lazy load image previews only when visible in viewport
- File Uploads:
- Compress images client-side before upload (if enabled)
- Support progressive JPEGs for better perceived loading
Font Loading Strategy
- Upload UI Fonts:
- Preload critical font weights (400, 500) for upload area
- Use font-display: swap to prevent FOIT
- Fallback to system fonts for immediate rendering
Memoization Strategy
- useCallback:
- File validation functions (size, type)
- Drag event handlers (onDragOver, onDrop)
- Progress calculation functions
- useMemo:
- File list transformations (filtered, sorted)
- Progress percentage calculations
- Preview URL generation (when using FileReader API)
- React.memo:
- FileItem component (re-renders only when file object changes)
- Progress bar component (re-renders only when progress value changes)
Virtualization for Large Lists
- Not applicable for typical file upload use cases (usually 1-5 files per upload)
- Consideration: If supporting batch uploads with >50 files, implement virtualization using
react-windoworreact-virtual - Current strategy: Render all files in DOM but optimize rendering with proper memoization and conditional rendering
Prefetching and Preloading Strategy
- Preload:
- Upload API endpoint metadata (if using server-side pre-signed URLs)
- Critical assets like icons and progress bar SVGs
- Prefetch:
- File validation rules (type, size) from server for client-side validation
- Image preview library (if using external image processing)
- Lazy loading:
- File previews only when user scrolls to that file item
- Progress indicators only when file is actively uploading
// Performance-optimized file upload component example
const FileUpload = React.memo(() => {
const [files, setFiles] = useState<File[]>([]);
const [uploadProgress, setUploadProgress] = useState<Record<string, number>>({});
const handleFileDrop = useCallback((acceptedFiles: File[]) => {
setFiles(prev => [...prev, ...acceptedFiles]);
}, []);
const calculateProgress = useCallback((fileId: string) => {
return uploadProgress[fileId] || 0;
}, [uploadProgress]);
return (
<div className="file-upload-container">
{files.map(file => (
<FileItem
key={file.name + file.size}
file={file}
progress={calculateProgress(file.name)}
/>
))}
</div>
);
});
const FileItem = React.memo(({ file, progress }: { file: File; progress: number }) => {
const [previewUrl, setPreviewUrl] = useState<string | null>(null);
useEffect(() => {
if (file.type.startsWith('image/')) {
const reader = new FileReader();
reader.onload = () => setPreviewUrl(reader.result as string);
reader.readAsDataURL(file);
}
}, [file]);
return (
<div className="file-item">
{previewUrl && (
<img
src={previewUrl}
alt={file.name}
loading="lazy"
className="file-preview"
/>
)}
<div className="file-info">
<span className="file-name">{file.name}</span>
<ProgressBar progress={progress} />
</div>
</div>
);
});
7. Error Handling & Resilience
Error Boundary Placement Strategy
Error boundaries should be strategically placed to prevent cascading failures while maintaining user experience. For a file upload component, the primary error boundary should wrap the entire upload flow to catch unexpected runtime errors.
graph TD
A[FileUploadComponent] --> B[ErrorBoundary]
B --> C[UploadForm]
B --> D[FilePreview]
B --> E[ProgressIndicator]
B --> F[UploadStatus]
Global Error Handler Design
A global error handler will capture unhandled promise rejections and runtime errors:
// ErrorBoundary.tsx
import React from 'react';
interface ErrorBoundaryState {
hasError: boolean;
error?: Error;
errorInfo?: React.ErrorInfo;
}
class ErrorBoundary extends React.Component<{}, ErrorBoundaryState> {
constructor(props: {}) {
super(props);
this.state = { hasError: false };
}
static getDerivedStateFromError(error: Error): ErrorBoundaryState {
return { hasError: true, error };
}
componentDidCatch(error: Error, errorInfo: React.ErrorInfo) {
console.error('File upload error:', error, errorInfo);
// Log to monitoring service
this.setState({ errorInfo });
}
render() {
if (this.state.hasError) {
return (
<div className="error-container">
<h2>Something went wrong with file upload.</h2>
<button onClick={() => window.location.reload()}>
Reload Page
</button>
</div>
);
}
return this.props.children;
}
}
export default ErrorBoundary;
Offline / Network Failure Handling
Implement network status detection and graceful degradation:
// useNetworkStatus.ts
import { useState, useEffect } from 'react';
export const useNetworkStatus = () => {
const [isOnline, setIsOnline] = useState<boolean>(true);
useEffect(() => {
const handleOnline = () => setIsOnline(true);
const handleOffline = () => setIsOnline(false);
window.addEventListener('online', handleOnline);
window.addEventListener('offline', handleOffline);
return () => {
window.removeEventListener('online', handleOnline);
window.removeEventListener('offline', handleOffline);
};
}, []);
return isOnline;
};
// FileUploadComponent.tsx
const FileUpload = () => {
const isOnline = useNetworkStatus();
if (!isOnline) {
return (
<div className="offline-message">
<p>Offline mode - Upload will resume when connection is restored</p>
<button onClick={resumeUpload}>Resume</button>
</div>
);
}
// Normal upload flow
};
Graceful Degradation Patterns
Support for browsers without required APIs:
// FileUploadComponent.tsx (partial)
const FileUpload = () => {
const [supportsDragAndDrop, setSupportsDragAndDrop] = useState<boolean>(true);
const [supportsFileAPI, setSupportsFileAPI] = useState<boolean>(true);
useEffect(() => {
// Check browser support
setSupportsDragAndDrop(
typeof window !== 'undefined' &&
'draggable' in document.createElement('div')
);
setSupportsFileAPI(
typeof window !== 'undefined' &&
!!window.File &&
!!window.FileReader &&
!!window.FileList
);
}, []);
if (!supportsFileAPI) {
return (
<div className="unsupported-browser">
<p>Your browser does not support file uploads.</p>
<a href="/upgrade-browser">Upgrade your browser</a>
</div>
);
}
return (
<div className="upload-container">
{supportsDragAndDrop ? <DragDropUploader /> : <TraditionalUploader />}
</div>
);
};
User Feedback on Errors
Implement toast notifications and inline error messages:
// useToast.ts
import { useState } from 'react';
interface Toast {
id: string;
message: string;
type: 'success' | 'error' | 'warning' | 'info';
duration?: number;
}
export const useToast = () => {
const [toasts, setToasts] = useState<Toast[]>([]);
const addToast = (message: string, type: Toast['type'] = 'info', duration = 5000) => {
const id = Date.now().toString();
setToasts(prev => [...prev, { id, message, type, duration }]);
if (type === 'error') {
// Log to monitoring service
console.error(`Toast error: ${message}`);
}
};
const removeToast = (id: string) => {
setToasts(prev => prev.filter(toast => toast.id !== id));
};
return { toasts, addToast, removeToast };
};
// FileUploadComponent.tsx (partial)
const FileUpload = () => {
const { addToast } = useToast();
const [uploadErrors, setUploadErrors] = useState<string[]>([]);
const handleFileUpload = async (files: File[]) => {
try {
// Upload logic
const response = await uploadFiles(files);
if (response.status === 'success') {
addToast('Upload completed successfully!', 'success');
}
} catch (error) {
const errorMessage = error instanceof Error ? error.message : 'Upload failed';
addToast(errorMessage, 'error');
setUploadErrors(prev => [...prev, errorMessage]);
}
};
};
Retry Strategy Implementation
Implement exponential backoff for failed uploads:
// useRetry.ts
import { useState, useEffect } from 'react';
export const useRetry = (
fn: () => Promise<any>,
maxRetries = 3,
delay = 1000
) => {
const [retryCount, setRetryCount] = useState(0);
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState<Error | null>(null);
const executeWithRetry = async () => {
setIsLoading(true);
setError(null);
let lastError: Error | null = null;
for (let attempt = 0; attempt <= maxRetries; attempt++) {
try {
const result = await fn();
setIsLoading(false);
return result;
} catch (err) {
lastError = err as Error;
if (attempt < maxRetries) {
const waitTime = delay * Math.pow(2, attempt);
await new Promise(resolve => setTimeout(resolve, waitTime));
}
}
}
setIsLoading(false);
setError(lastError);
return null;
};
return {
executeWithRetry,
retryCount,
isLoading,
error
};
};
Comprehensive Error Handling Strategy
// FileUploadErrorTypes.ts
export type UploadErrorType =
| 'NETWORK_ERROR'
| 'FILE_SIZE_EXCEEDED'
| 'INVALID_FILE_TYPE'
| 'UPLOAD_CANCELLED'
| 'SERVER_ERROR'
| 'UNKNOWN_ERROR';
export interface UploadError {
type: UploadErrorType;
message: string;
file?: File;
timestamp: Date;
}
// Error handling in main component
const FileUpload = () => {
const [errors, setErrors] = useState<UploadError[]>([]);
const handleFileValidation = (file: File): UploadError | null => {
// Size validation
if (file.size > MAX_FILE_SIZE) {
return {
type: 'FILE_SIZE_EXCEEDED',
message: `File ${file.name} exceeds maximum size of ${MAX_FILE_SIZE / (1024 * 1024)}MB`,
file,
timestamp: new Date()
};
}
// Type validation
if (!ALLOWED_FILE_TYPES.includes(file.type)) {
return {
type: 'INVALID_FILE_TYPE',
message: `File ${file.name} has unsupported type`,
file,
timestamp: new Date()
};
}
return null;
};
const handleError = (error: UploadError) => {
setErrors(prev => [...prev, error]);
// Add toast notification
addToast(error.message, 'error');
};
};
8. Testing Strategy
Unit Testing Approach
Component Tests (70% coverage target):
- FileUpload component with various states (idle, uploading, success, error)
- DragAndDrop zone with hover/focus states
- Progress bar component with different percentage values
- File preview component for various file types (image, document, video)
- Validation logic tests (file size limits, mime type checks)
Utility Tests (80% coverage target):
- File validation helpers (size, type, extension checks)
- MIME type detection utilities
- File size formatting functions (KB, MB, GB)
- Drag and drop event handling utilities
Test Coverage Targets:
- Components: 70% line coverage, 60% branch coverage
- Utilities: 85% line coverage, 80% branch coverage
- Edge cases: 100% for error conditions and boundary values
Integration Testing Strategy
Feature Integration Tests (50% of integration tests):
- Complete upload flow from drag/drop to success state
- Multi-file upload scenarios with mixed file types
- Concurrent uploads with progress tracking
- Error recovery flows (retry, cancel, resume)
- Validation error propagation through UI
API Integration Tests:
- Mocked upload endpoints with various response codes (200, 400, 401, 500)
- Upload progress simulation with realistic timing
- Large file chunked upload handling
- Concurrent request limits enforcement
E2E Testing Scope
End-to-End Test Coverage (Playwright):
- Primary user flows:
- Drag and drop file upload
- Click-to-upload file selection
- Multi-file upload with mixed types
- Error handling flows:
- File size limit exceeded
- Invalid file type upload
- Network interruption during upload
- Accessibility testing:
- Keyboard navigation through upload UI
- Screen reader compatibility verification
Critical User Journeys:
- Upload single image file → preview → success
- Upload multiple documents → progress tracking → completion
- Cancel ongoing upload → reset UI state
- Retry failed upload → error recovery
Visual Regression Testing
Screenshot Strategy:
- Component states (idle, hover, active, error, success)
- Different file types (image, document, video, audio)
- Mobile vs. desktop viewports (375px, 768px, 1200px)
- Dark/light mode variations
Tools:
- Percy or Chromatic for visual regression
- Baseline snapshots for each component variant
Accessibility Testing Automation
Automated A11y Checks:
- axe-core integration in component tests (50% of test suite)
- Keyboard navigation testing with Playwright
- Focus order validation for all interactive elements
WCAG Compliance Targets:
- AA level compliance for all interactive elements
- Sufficient color contrast (4.5:1 ratio)
- Proper semantic HTML structure
- Screen reader friendly labeling
Test Data Strategy
Test Fixtures:
// Sample test data structure
const mockFiles = {
validImage: new File([new ArrayBuffer(1024)], 'test.jpg', { type: 'image/jpeg' }),
largeFile: new File([new ArrayBuffer(1024 * 1024 * 10)], 'large.pdf', { type: 'application/pdf' }),
invalidType: new File([new ArrayBuffer(1024)], 'test.txt', { type: 'text/plain' }),
multipleFiles: [
new File([new ArrayBuffer(1024)], 'doc1.pdf', { type: 'application/pdf' }),
new File([new ArrayBuffer(2048)], 'doc2.docx', { type: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document' })
]
};
Mock API Responses:
- Success responses with various file sizes
- Error responses (400, 401, 500) with appropriate messages
- Timeout scenarios for network failure simulation
Test Environment Setup:
- Jest configuration with DOM environment
- Playwright browser context with mobile emulation
- Mock service worker (MSW) for API mocking in E2E tests
9. Accessibility (A11y)
WCAG 2.1 AA compliance checklist for this feature
The file upload component must meet all Level AA success criteria from WCAG 2.1:
- 1.1.1 Non-text Content (Level A): All non-text content must have text alternatives, including the upload area and file list items.
- 1.3.1 Info and Relationships (Level A): Content structure must be semantically correct using HTML elements like
<form>,<input>, and proper labeling. - 1.3.3 Sensory Characteristics (Level AA): Instructions must not rely solely on sensory characteristics like color or shape.
- 1.4.1 Use of Color (Level A): Color must not be the sole method for conveying information or indicating state.
- 1.4.3 Contrast (Minimum) (Level AA): Text and controls must meet 4.5:1 contrast ratio.
- 2.1.1 Keyboard (Level A): All functionality must be operable via keyboard.
- 2.4.1 Bypass Blocks (Level A): Skip links or navigation structure must allow bypassing the upload area.
- 3.1.1 Language of Page (Level AA): Language must be identified for screen readers.
- 4.1.2 Name, Role, Value (Level AA): All interactive elements must have proper ARIA roles and accessible names.
Keyboard navigation support
The component supports full keyboard navigation:
- Tab navigation: Moves focus through all interactive elements (upload button, file list items, remove buttons)
- Space/Enter: Activates the upload area for file selection
- Delete/Backspace: Removes selected files from the list
- Escape: Cancels ongoing uploads or closes modals (if applicable)
- Focus management: Proper focus order maintains logical reading sequence
Focus management strategy
stateDiagram-v2
[*] --> Idle
Idle --> UploadFocused : Tab into upload area
UploadFocused --> FileList : Enter or Space to open file dialog
FileList --> FileItem : Tab through files
FileItem --> RemoveButton : Tab to remove button
RemoveButton --> FileItem : Remove completes, focus returns to file list
FileItem --> UploadFocused : Tab to upload area (wrap)
Focus is managed with:
- Auto-focus on the first interactive element after upload completion
- Focus restoration when removing files or resetting state
- Proper
tabindexvalues (0 for interactive elements, -1 for hidden elements) - Focus trapping within modals or dialogs when present
ARIA roles and attributes plan
// FileUpload component ARIA attributes
const fileUploadProps = {
role: "region",
"aria-label": "File upload area",
"aria-describedby": "upload-hint",
"aria-invalid": hasErrors ? "true" : "false"
};
// File item ARIA
const fileItemProps = {
role: "listitem",
"aria-label": `${fileName} (${fileSize})`,
"aria-describedby": `file-status-${id}`
};
// Upload button ARIA
const uploadButtonProps = {
role: "button",
"aria-label": "Select files to upload",
"aria-describedby": "upload-hint"
};
Key ARIA roles and attributes:
role="region"for the upload containeraria-labelfor all interactive elements without visible textaria-describedbyto associate help text and status informationaria-invalidfor error statesaria-livefor real-time upload progress updatesaria-busyduring file processing
Screen reader compatibility
The component is compatible with major screen readers:
- NVDA (Windows): Full support for all ARIA attributes and keyboard navigation
- JAWS (Windows): All interactive elements announced with proper context
- VoiceOver (macOS): Proper reading order and semantic structure maintained
- TalkBack (Android): Touch navigation and ARIA labels properly interpreted
Keyboard focus indicators are visually distinct from default browser focus styles to ensure screen reader users can identify the current focus.
The component provides:
- Real-time status updates via
aria-live="polite" - Clear file name and size announcements
- Error messages with context for screen reader users
- Proper announcement of upload progress percentages
- Descriptive feedback when files are added or removed
All interactive elements have appropriate aria-label or aria-labelledby to ensure screen readers announce the correct purpose and state of each element. The component avoids relying on color alone to indicate file status or upload progress, ensuring compatibility with users who are colorblind or have low vision.
10. Security Considerations
XSS Prevention Strategy
The file upload component must prevent cross-site scripting attacks through comprehensive input sanitization and output encoding:
// Secure file name handling to prevent XSS
const sanitizeFileName = (fileName: string): string => {
// Remove dangerous characters that could enable XSS
return fileName
.replace(/</g, '<')
.replace(/>/g, '>')
.replace(/"/g, '"')
.replace(/'/g, ''')
.replace(/\//g, '/')
.replace(/\\/g, '\');
};
// Secure file content validation
const validateFileContent = (file: File): boolean => {
// Check for malicious content patterns in file headers
const content = new Uint8Array(file.slice(0, 1024).arrayBuffer());
// Block known malicious file signatures
const maliciousPatterns = [
[0x50, 0x4B, 0x05, 0x06], // ZIP with empty central directory
[0x4D, 0x5A], // DOS executable
];
return !maliciousPatterns.some(pattern =>
pattern.every((byte, i) => content[i] === byte)
);
};
CSRF Protection
Implement CSRF token validation to prevent cross-site request forgery:
// File upload form with CSRF protection
const FileUploadForm = () => {
const [csrfToken] = useCSRFToken();
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
const formData = new FormData(e.target as HTMLFormElement);
formData.append('csrf_token', csrfToken);
try {
const response = await fetch('/api/upload', {
method: 'POST',
body: formData,
headers: {
'X-CSRF-Token': csrfToken,
},
});
if (!response.ok) {
throw new Error('Upload failed');
}
} catch (error) {
// Handle CSRF error specifically
if (error.response?.status === 403) {
// Redirect to login or refresh CSRF token
window.location.reload();
}
}
};
return (
<form onSubmit={handleSubmit} encType="multipart/form-data">
{/* File input */}
<input type="file" name="file" required />
<button type="submit">Upload</button>
</form>
);
};
Content Security Policy (CSP) Headers
Configure CSP headers to restrict file upload content:
Content-Security-Policy:
default-src 'self';
script-src 'self' 'unsafe-inline';
object-src 'none';
img-src 'self' data: blob:;
form-action 'self';
Sensitive Data Handling in Frontend State
Protect sensitive file metadata from being exposed:
// Secure file state management
interface SecureFileState {
// Only store non-sensitive metadata
id: string;
name: string;
size: number;
type: string;
status: 'pending' | 'uploading' | 'success' | 'error';
// Never store sensitive fields in component state
// filePath: string; // ❌ Don't store file paths or full metadata
// fileBuffer: ArrayBuffer; // ❌ Don't store raw file data
progress?: number;
uploadUrl?: string;
}
// Secure file handling with minimal state
const useSecureFileUpload = () => {
const [files, setFiles] = useState<SecureFileState[]>([]);
const addFile = (file: File) => {
// Validate file before adding to state
if (!validateFileContent(file)) {
throw new Error('Invalid file content');
}
const secureFile: SecureFileState = {
id: generateId(),
name: sanitizeFileName(file.name),
size: file.size,
type: file.type,
status: 'pending',
};
setFiles(prev => [...prev, secureFile]);
};
return { files, addFile };
};
Input Sanitization Approach
Implement comprehensive input sanitization for all user-provided data:
// Comprehensive file validation and sanitization
const validateAndSanitizeUpload = (file: File): Promise<ValidatedFile> => {
return new Promise((resolve, reject) => {
// Size validation
if (file.size > MAX_FILE_SIZE) {
reject(new Error('File too large'));
return;
}
// Type validation
if (!ALLOWED_MIME_TYPES.includes(file.type)) {
reject(new Error('Invalid file type'));
return;
}
// Name sanitization
const sanitizedFileName = sanitizeFileName(file.name);
// Extension validation
const fileExtension = sanitizedFileName.split('.').pop()?.toLowerCase();
if (!ALLOWED_EXTENSIONS.includes(`.${fileExtension}`)) {
reject(new Error('Invalid file extension'));
return;
}
// Content validation (for security)
const reader = new FileReader();
reader.onload = () => {
// Validate file content (e.g., image dimensions, document headers)
const content = reader.result as ArrayBuffer;
// Reject files with suspicious content
if (isMaliciousContent(content)) {
reject(new Error('File contains malicious content'));
return;
}
resolve({
file: file,
name: sanitizedFileName,
size: file.size,
type: file.type,
extension: fileExtension,
});
};
reader.onerror = () => reject(new Error('Failed to read file'));
reader.readAsArrayBuffer(file.slice(0, 1024)); // Read first 1KB for validation
});
};
// File content validator
const isMaliciousContent = (content: ArrayBuffer): boolean => {
const view = new Uint8Array(content);
// Check for known malicious signatures
const maliciousSignatures = [
[0x50, 0x4B, 0x05, 0x06], // ZIP with empty central directory
[0x4D, 0x5A], // DOS executable
[0x89, 0x50, 0x4E, 0x47], // PNG header (should not be in executable context)
];
return maliciousSignatures.some(signature =>
signature.every((byte, i) => view[i] === byte)
);
};
Security Headers Implementation
Implement security headers for upload endpoints:
// Server-side security headers (example)
const secureUploadHeaders = {
'X-Content-Type-Options': 'nosniff',
'X-Frame-Options': 'DENY',
'X-XSS-Protection': '1; mode=block',
'Content-Security-Policy': "default-src 'self'; img-src 'self' data: blob:;",
'Referrer-Policy': 'strict-origin-when-cross-origin',
};
File Upload Security Best Practices
- Input Validation: All file uploads must be validated server-side, never rely solely on client-side validation
- File Type Restrictions: Implement strict MIME type and extension filtering
- Size Limits: Enforce maximum file size limits to prevent resource exhaustion
- Content Inspection: Scan uploaded files for malicious content patterns
- Secure Storage: Store uploaded files in secure, non-executable locations
- Access Control: Implement proper file access controls and authentication checks
- Rate Limiting: Prevent abuse through upload rate limiting per user/session
Threat Model Considerations
The component must guard against:
- File Upload Vulnerabilities: Arbitrary file execution, directory traversal
- Malware Injection: Files containing malicious code or scripts
- Resource Exhaustion: Large file uploads consuming server resources
- Bypass Attacks: Attempting to circumvent client-side validation
- Information Disclosure: Exposing sensitive file metadata or paths
All security measures should be implemented in conjunction with server-side validation and proper file handling practices.
11. Internationalization (i18n)
Translation Key Structure and Naming Convention
Following the BEM-style naming convention with feature-scoped keys:
// Translation keys for file upload component
{
"fileUpload": {
"dropZone": {
"label": "Drop files here or click to browse",
"description": "Supports {{supportedFormats}} up to {{maxSize}}",
"supportedFormats": "PDF, DOCX, JPG",
"maxSize": "10MB"
},
"fileList": {
"emptyState": "No files uploaded yet",
"uploading": "Uploading...",
"success": "Upload complete",
"error": "Upload failed"
},
"uploadButton": {
"label": "Select Files",
"ariaLabel": "Upload files button"
},
"progress": {
"status": "{{percent}}% complete",
"remaining": "{{timeRemaining}} remaining"
},
"errors": {
"sizeExceeded": "File exceeds maximum size of {{maxSize}}",
"typeNotAllowed": "File type {{fileType}} not allowed",
"uploadFailed": "Upload failed. Please try again."
}
}
}
RTL Support Considerations
RTL layout adjustments:
- Drop zone text alignment:
text-align: rightin RTL mode - Icon rotation for upload button:
transform: rotate(180deg)in RTL context - Progress bar direction:
direction: rtlwithunicode-bidi: embed - File list item layout: reverse order in RTL mode
Date, Number, Currency Formatting Strategy
Using Intl API for locale-aware formatting:
// Format file size in localized units
const formatFileSize = (bytes: number, locale: string): string => {
const units = ['B', 'KB', 'MB', 'GB'];
const unitIndex = Math.floor(Math.log(bytes) / Math.log(1024));
const size = bytes / Math.pow(1024, unitIndex);
return new Intl.NumberFormat(locale, {
maximumFractionDigits: 1,
style: 'unit',
unit: units[unitIndex]
}).format(size);
};
// Format time remaining
const formatTimeRemaining = (seconds: number, locale: string): string => {
const minutes = Math.floor(seconds / 60);
const remainingSeconds = seconds % 60;
return new Intl.RelativeTimeFormat(locale, { style: 'short' }).format(
-minutes,
'minute'
);
};
Locale Detection and Switching Strategy
Automatic locale detection using browser navigator:
// Auto-detect locale from browser settings
const detectLocale = (): string => {
const browserLang = navigator.language || 'en-US';
const supportedLocales = ['en-US', 'fr-FR', 'es-ES', 'de-DE'];
// Match to supported locales or fallback to default
return supportedLocales.find(locale =>
locale.startsWith(browserLang.split('-')[0])
) || 'en-US';
};
// Locale switching with persistence
const useLocaleSwitcher = () => {
const [currentLocale, setCurrentLocale] = useState<string>(
localStorage.getItem('preferredLocale') || detectLocale()
);
const switchLocale = (newLocale: string) => {
setCurrentLocale(newLocale);
localStorage.setItem('preferredLocale', newLocale);
};
return { currentLocale, switchLocale };
};
Translation Key Management
Using a hierarchical structure to support:
- Feature-level grouping (
fileUpload.*) - Context-aware variations (
errors.sizeExceeded) - Dynamic content placeholders (
{{supportedFormats}})
Translation files organized by language:
src/locales/
├── en-US.json
├── fr-FR.json
└── es-ES.json
i18n Integration Pattern
Component-level translation using React-i18next:
import { useTranslation } from 'react-i18next';
const FileUploadComponent = () => {
const { t, i18n } = useTranslation('fileUpload');
// Use translated strings with dynamic content
const fileSizeText = t('dropZone.description', {
supportedFormats: t('supportedFormats'),
maxSize: formatFileSize(maxSizeBytes, i18n.language)
});
return (
<div>
<p>{fileSizeText}</p>
<button aria-label={t('uploadButton.ariaLabel')}>
{t('uploadButton.label')}
</button>
</div>
);
};
12. Monitoring & Observability
Real User Monitoring (RUM) Metrics to Track
The file upload component should capture key performance indicators for user experience monitoring:
- Upload Duration: Time from start of upload to completion (target < 2s for 90th percentile)
- Upload Speed: Bytes per second during upload (target > 10KB/s for 90th percentile)
- Upload Success Rate: Percentage of successful uploads vs total attempts (target > 95%)
- File Size Distribution: Histogram of uploaded file sizes (target: <10MB files should be >90%)
- Error Rate by Type: Breakdown of upload failures (network, validation, server errors)
- Concurrent Uploads: Number of simultaneous uploads per user (target: <5 concurrent uploads)
- Retry Count Distribution: Average number of retry attempts per upload (target: <2 retries)
Error Tracking Integration
Integrate with error tracking platforms like Sentry or Datadog to capture:
- Upload Failure Events: Network timeouts, server errors, validation failures
- Client-Side Errors: File type mismatches, size limits exceeded, browser compatibility issues
- User Action Context: Upload ID, file metadata, user session, timestamp
- Stack Traces: Full error stack traces with contextual information
Custom Events and User Journey Tracking
Implement custom event tracking for user upload flows:
// Event tracking interface
interface UploadEvent {
eventType: 'upload_started' | 'upload_progress' | 'upload_success' | 'upload_failed';
uploadId: string;
fileSize: number;
fileType: string;
duration?: number;
errorType?: string;
retryCount?: number;
}
// Example implementation
const trackUploadEvent = (event: UploadEvent) => {
// Send to analytics service (e.g., Mixpanel, Amplitude)
analytics.track('file_upload', event);
};
Performance Monitoring and Regression Detection
Set up performance baselines to detect regressions:
- LCP (Largest Contentful Paint): Track upload UI rendering performance
- FID (First Input Delay): Monitor responsiveness during drag-and-drop interactions
- CLS (Cumulative Layout Shift): Ensure upload UI stability during progress updates
- Bundle Size: Monitor file upload component bundle size changes (target: <5KB delta)
- Memory Usage: Track memory consumption during large file uploads
Observability Dashboard Components
Create dashboard metrics for monitoring:
- Upload Pipeline Health: Success/failure rates, retry patterns
- User Behavior Analytics: Upload frequency, file size trends, concurrent uploads
- Infrastructure Metrics: API response times, server error rates, storage capacity
- Error Distribution: By file type, size, user location, and browser
Alerting Strategy
Configure alerts for:
- Upload Success Rate Drop: <95% success rate over 5-minute window
- Upload Duration Spike: >2s average upload time for 10%+ increase
- API Error Rate: >5% error rate from upload endpoint
- Concurrent Upload Limit: >80% of max concurrent uploads in use
Data Collection and Privacy
- Anonymous User Tracking: Use anonymous identifiers for user journey tracking
- PII Sanitization: Never log file content, user paths, or sensitive metadata
- Compliance: Ensure compliance with GDPR and CCPA for data collection
- Opt-out Mechanism: Allow users to disable analytics tracking
Integration with Existing Monitoring Stack
The component should integrate with existing observability tools:
// Example integration pattern
const uploadMonitor = {
trackUploadStart: (uploadId: string, fileSize: number) => {
// Send to RUM service
rum.track('upload_start', { uploadId, fileSize });
// Send to error tracking
Sentry.captureMessage('Upload started', {
extra: { uploadId, fileSize }
});
},
trackUploadComplete: (uploadId: string, duration: number, success: boolean) => {
// Send performance metrics
performance.mark(`upload_${uploadId}_complete`);
// Send to analytics
analytics.track('upload_complete', {
uploadId,
duration,
success,
timestamp: Date.now()
});
}
};
Telemetry Configuration
Support configurable telemetry levels:
- Basic: Only success/failure metrics
- Detailed: Include file metadata, performance data
- Full: Include all user actions and system state
type TelemetryLevel = 'basic' | 'detailed' | 'full';
interface UploadTelemetryConfig {
level: TelemetryLevel;
enabled: boolean;
samplingRate?: number; // For detailed/full levels
}
This monitoring approach ensures comprehensive visibility into file upload performance, user experience, and system health while maintaining privacy and compliance standards.
13. File & Folder Structure (brief — implementation detail, not primary LLD interview content)
Proposed top-level directory structure
I recommend a feature-based folder structure for optimal maintainability and team ownership:
/src
/components
/FileUpload
FileUpload.tsx
FilePreview.tsx
UploadProgress.tsx
index.ts
/hooks
useFileUpload.ts
/types
fileUpload.types.ts
/utils
fileValidation.ts
fileUploadHelpers.ts
/services
fileUploadService.ts
Module boundaries and import rules
- No cross-feature imports: Each feature owns its dependencies
- Barrel exports:
components/FileUpload/index.tsexports all public components - Shared utilities: Common helpers live in
/utilsand are imported by features as needed
Shared vs. feature-specific code separation
- Shared: Validation logic, error handling utilities, common types
- Feature-specific: Component implementations, service integrations, custom hooks
- Reusability: Components are designed to be reusable across features with proper prop contracts
This structure enables clear ownership, easy testing, and scalable evolution of the file upload feature while maintaining clean separation from other system concerns.
14. Dependencies & Third-Party Libraries
Key Libraries Selected with Justification
1. react-dropzone
- Justification: Provides robust, accessible drag-and-drop file upload capabilities with comprehensive browser support. Handles cross-browser file selection, drag events, and keyboard accessibility out of the box.
- Bundle size: ~15KB gzipped
- Alternatives considered:
- Custom implementation: Would require extensive boilerplate for accessibility, drag handling, and cross-browser compatibility
- react-beautiful-dnd: Overkill for file uploads, focused on reorderable lists
2. axios
- Justification: Industry-standard HTTP client with excellent request/response interception, automatic JSON parsing, and comprehensive error handling. Integrates well with React Query for data fetching.
- Bundle size: ~25KB gzipped
- Alternatives considered:
- fetch API: Missing built-in request/response interceptors and better error handling
- ky: Too minimal for complex upload scenarios with progress tracking
3. react-query (TanStack Query)
- Justification: Excellent for managing server state, caching, and automatic refetching. Handles concurrent uploads, optimistic updates, and proper error boundaries.
- Bundle size: ~20KB gzipped
- Alternatives considered:
- Redux Toolkit: Too heavy for simple state management needs, overkill for file upload flow
- Zustand: Could work but lacks built-in caching and server-state management features
4. react-hook-form
- Justification: Provides robust form validation and state management for file upload forms with built-in error handling and accessibility features.
- Bundle size: ~25KB gzipped
- Alternatives considered:
- Custom form handling: Would require extensive custom validation logic and error state management
Alternatives Considered and Rejected
1. react-file-upload
- Rejected: Outdated with poor accessibility support and limited customization options
- Reason: No active maintenance, lacks drag-and-drop features, poor TypeScript support
2. react-dropzone with custom validation
- Rejected: Would require extensive custom implementation of validation logic and error handling
- Reason: Inefficient use of time when proven solutions exist
3. Webpack's file-loader
- Rejected: Not a frontend library, only handles bundling, not runtime upload handling
- Reason: Misplaced in frontend component design, serves different architectural purpose
Licensing and Bundle Size Impact Assessment
Bundle size impact: ~85KB total (gzipped) for core dependencies, which is acceptable for a file upload feature. This represents roughly 2-3% of a typical application's frontend bundle size.
Licensing considerations:
- All selected libraries are MIT licensed with no commercial restrictions
- react-dropzone and axios have permissive licenses suitable for enterprise use
- react-query has MIT license with commercial usage rights
Tree-shaking potential: All dependencies support tree-shaking, allowing for minimal bundle inclusion of only required modules.
Performance Impact Considerations
Critical path optimization:
- All libraries are designed for performance with minimal overhead
- react-dropzone uses efficient event handling and DOM manipulation
- react-query provides automatic caching and deduplication to reduce unnecessary API calls
Bundle size budget:
- Target: < 100KB total for all dependencies (including React ecosystem)
- Current estimate: ~85KB gzipped, well within acceptable limits
- Future optimization opportunities: Selective imports from react-hook-form and react-query
Third-party integration considerations
API client integration: axios provides excellent interceptor support for authentication token injection and response normalization, making it ideal for enterprise-grade file upload systems.
TypeScript support: All selected libraries have excellent TypeScript typing with no additional configuration required, ensuring type safety throughout the upload flow.
Testing compatibility: All libraries have established testing patterns and are compatible with Jest and React Testing Library, supporting comprehensive test coverage.
15. Open Questions, Risks & Trade-offs
Decisions that need product/engineering alignment
- File size limits: What's the maximum file size allowed? This directly impacts UI design (progress indicators, upload queue limits) and backend architecture (chunked uploads vs. single requests).
- Supported file types: Will we support all file types or restrict to specific extensions? This affects validation logic, preview capabilities, and security considerations.
- Upload retry behavior: Should failed uploads automatically retry or require manual intervention? This impacts error handling complexity and user expectations.
- Multiple vs. single file upload: Will the component support multiple simultaneous uploads? This affects state management, queue handling, and UI complexity.
Known trade-offs made in this design
- Browser compatibility vs. modern APIs: Using
FileReaderAPI for previews andFormDatafor uploads provides good browser support but limits access to newer features likeFileSystem APIor Web Workers for large file handling. - Client-side validation vs. server-side enforcement: While we implement client-side validation for UX, all file validation must still happen server-side to prevent security issues.
- UI state complexity vs. user experience: The multi-state UI (idle, uploading, success, error) adds complexity but provides better user feedback than a simple "uploading" indicator.
- Memory usage vs. performance: Previews for large files may cause memory issues; we balance this with providing useful feedback to users.
Risks and mitigations
- Large file upload failures: Risk of timeouts or memory issues with large files. Mitigation: Implement chunked uploads with progress tracking and cancellation support.
- Security vulnerabilities: Risk of malicious file uploads. Mitigation: Server-side validation, content type checking, and virus scanning integration.
- Browser memory exhaustion: Risk of OOM errors when processing large files. Mitigation: Implement file size limits and use streaming APIs where possible.
- Network instability: Risk of partial uploads due to unstable connections. Mitigation: Implement resume capability and automatic retry logic with exponential backoff.
Technical debt acknowledged
- Limited preview support: Current implementation only supports image previews. Future work needed to support video, audio, and document previews.
- Basic error handling: Simple toast notifications instead of comprehensive error categorization and user guidance.
- No drag-and-drop fallback: The component assumes modern browser support for drag-and-drop, which may not be available in older browsers.
- No progressive enhancement: The component is built for modern browsers and doesn't provide graceful degradation for older environments.
- Hardcoded UI strings: Translation keys are not yet implemented, requiring future refactoring to support i18n.
- No accessibility testing: While ARIA attributes are included, comprehensive accessibility testing with screen readers and keyboard navigation is not yet implemented.