Skip to content

File Upload with Progress

Pick or drop a file, upload it with axios, and show progress, success and error states in TypeScript.

Stage 4core3 min read

Problem Statement

Build a file upload box. The user picks a file or drops it on the box, sees its details, uploads it, and sees the upload progress and the result.

Functional Requirements

  1. Pick or drop: Clicking the dashed box opens the file picker. Dropping a file on it also selects it. Only the first file is used.
  2. Drag feedback: The box turns blue while a file is dragged over it.
  3. File details: Show the selected file's name, size in KB and type, or Unknown if the type is empty.
  4. Upload: The Upload button posts the file as FormData under the file key to https://httpbin.org/post with axios.
  5. Progress: While uploading, hide the button and show a bar and a percent from onUploadProgress.
  6. Result: Show a success or an error message when the request ends. Picking a new file clears it.

Technical Constraints

  • Written in TypeScript. Upload state is one of "idle" | "uploading" | "success" | "error".
  • One file at a time. No type or size checks and no cancel.

Overview

A React component that handles file uploads with progress tracking, status management, and drag-and-drop functionality.

Core Functionality

State Management

const [file, setFile] = useState<File | null>(null);
const [status, setStatus] = useState<UploadStatus>("idle");
const [uploadProgress, setUploadProgress] = useState(0);
const [dragActive, setDragActive] = useState(false);

type UploadStatus = "idle" | "uploading" | "success" | "error";
  • file: Stores the selected file object
  • status: Tracks upload state ("idle", "uploading", "success", "error")
  • uploadProgress: Tracks upload progress (0-100)
  • dragActive: Manages drag-and-drop visual feedback

Key Functions Explained

1. File Selection Handler

const handleFileChange = (e: ChangeEvent<HTMLInputElement>) => {
  if (e.target.files) {
    setFile(e.target.files[0]);
    setStatus("idle");
  }
};
  • Triggered when user selects a file via input
  • e.target.files contains FileList object
  • We store the first file (files[0])
  • Status goes back to idle, so an old success or error message does not show for the new file

2. Drag and Drop Handlers

const handleDrag = (e: React.DragEvent) => {
  e.preventDefault();
  e.stopPropagation();
  setDragActive(e.type === "dragenter" || e.type === "dragover");
};

const handleDrop = (e: React.DragEvent) => {
  e.preventDefault();
  e.stopPropagation();
  setDragActive(false);

  if (e.dataTransfer.files?.[0]) {
    setFile(e.dataTransfer.files[0]);
    setStatus("idle");
  }
};
  • handleDrag: Manages drag state for visual feedback
  • handleDrop: Processes dropped file
  • e.dataTransfer.files contains dropped files

3. File Upload Handler

const handleFileUpload = async () => {
  if (!file) return null;
  setStatus("uploading");
  setUploadProgress(0);

  const formData = new FormData();
  formData.append("file", file);

  try {
    const response = await axios.post(URL, formData, {
      headers: {
        "Content-Type": "multipart/form-data",
      },
      onUploadProgress: (progressEvent) => {
        const progress = progressEvent?.total
          ? Math.round((progressEvent.loaded * 100) / progressEvent.total)
          : 0;
        setUploadProgress(progress);
      },
    });
    setStatus("success");
  } catch (error) {
    setStatus("error");
  }
};

Deep Dive into Key Concepts

FormData

const formData = new FormData();
formData.append("file", file);
  • FormData is a built-in browser API
  • Used to construct a set of key/value pairs representing form fields
  • Automatically sets proper headers for file upload

Example:

// Creating FormData
const formData = new FormData();

// Adding different types of data
formData.append("file", fileObject);
formData.append("username", "john_doe");
formData.append("age", "25");

// You can also append multiple files
formData.append("documents", file1);
formData.append("documents", file2);

onUploadProgress

onUploadProgress: (progressEvent) => {
  const progress = progressEvent?.total
    ? Math.round((progressEvent.loaded * 100) / progressEvent.total)
    : 0;
  setUploadProgress(progress);
};
  • Axios-specific callback for tracking upload progress
  • progressEvent.loaded: Bytes transferred
  • progressEvent.total: Total bytes to transfer
  • Calculates percentage: (loaded / total) * 100

Example of Progress Event:

// Sample progressEvent object
{
  loaded: 25000,  // bytes uploaded
  total: 100000,  // total file size in bytes
  progress: 0.25, // 25% complete
  bytes: 25000,   // same as loaded
  rate: 256000,   // bytes per second
  estimated: 2,   // seconds remaining
}

Code Flow

  1. Initial State

    • Component starts with null file and idle status
  2. File Selection

    • User selects file via click or drag-and-drop
    • File stored in state
    • File details displayed
  3. Upload Process

    Click Upload Button
    ↓
    Create FormData
    ↓
    Start Upload (Status: uploading)
    ↓
    Track Progress (onUploadProgress)
    ↓
    Handle Response
    ↓
    Update Status (success/error)
    
  4. Status Management

    • idle: Initial state or ready to upload
    • uploading: During file transfer
    • success: Upload completed
    • error: Upload failed

Error Handling

try {
  // upload logic
} catch (error) {
  setStatus("error");
  setUploadProgress(0);
}
  • Catches network errors
  • Catches server errors
  • Resets progress
  • Updates UI with error state

Common Interview Questions

"How would you handle multiple file uploads?"

Keep an array of files in state and read every selected file. Add multiple to the input.

const [files, setFiles] = useState<File[]>([]);

const handleFileChange = (e: ChangeEvent<HTMLInputElement>) => {
  if (e.target.files) {
    setFiles(Array.from(e.target.files));
  }
};

"How do you validate file types?"

Check file.type against an allow list before uploading. Also set accept on the input so the picker filters files, but do not rely on it alone.

const allowedTypes = ["image/jpeg", "image/png"];

const validateFile = (file: File) => {
  return allowedTypes.includes(file.type);
};

"How do you handle large file uploads?"

Reject files over a size limit first. For really large files, upload in chunks so a failed chunk can be retried and the upload can resume.

const MAX_SIZE = 5 * 1024 * 1024; // 5MB

if (file.size > MAX_SIZE) {
  alert("File too large");
  return;
}

"How do you cancel an upload in progress?"

Pass an AbortController signal to axios and call abort() from a Cancel button. Axios deprecated CancelToken, so use the signal instead.

const controller = new AbortController();

axios.post(URL, formData, { signal: controller.signal });

// To cancel
controller.abort();

In this topic

Done with this topic?

How well do you know it?