Skip to content
intermediatePhase 37 · Frontend Architecture

File Uploads

Handle file uploads with drag-and-drop, progress tracking, and preview.

45m
0 problems
Topic Progress0%

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

0/3solved
Build File Uploads Component

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 needed
File Uploads Testing

Write 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 tests
File Uploads Performance

Optimize 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 analysis

Quiz

1. Why validate files on the client before upload?

Question 1 options

2. What is the benefit of chunked uploads?

Question 2 options

3. Why use XMLHttpRequest instead of fetch for progress tracking?

Question 3 options

4. What should drag-and-drop zones handle?

Question 4 options

5. How should you reset the file input after selection?

Question 5 options

Flashcards

Question

Why validate files on the client?

Answer

To provide instant feedback, prevent invalid uploads, and reduce unnecessary server requests.

Question

What are the drag-and-drop events?

Answer

dragenter, dragleave, dragover, and drop events need to be handled.

Question

What is chunked upload?

Answer

Splitting large files into smaller chunks that can be uploaded individually and resumed.

Question

How do you track upload progress?

Answer

Use XMLHttpRequest's upload progress event or implement chunked progress tracking.

Question

What is File Uploads?

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