Tracking Upload Progress in Browsers

May 10, 2020

A short piece of skeleton code to demonstrate how to track upload progress in browsers.

import React, { useState } from "react";

const uploadUrl = "https://api.mysite.com/upload";

function App() {
  const [file, setFile] = useState<File>();
  const [uploadProgress, setUploadProgress] = useState<[number, number]>([
    0,
    0
  ]);
  return (
    <div className="App">
      <div>
        <button
          onClick={() => {
            if (file) {
              uploadFile(file, evt => {
                setUploadProgress([evt.loaded, evt.total]);
              });
            }
          }}
        >
          Upload file
        </button>
        <input
          type="file"
          name="myfile"
          onChange={e => {
            if (e.target.files && e.target.files.length > 0) {
              setFile(e.target.files[0]);
            }
          }}
        />
        {file && (
          <div>
            {uploadProgress[0]} of {uploadProgress[1]} bytes uploaded
          </div>
        )}
      </div>
    </div>
  );
}

const uploadFile: (
  file: File,
  progressCallback?: (x: ProgressEvent<EventTarget>) => void
) => Promise<string> = async (file, progressCallback) => {
  return new Promise((resolve, reject) => {
    const xhr = new XMLHttpRequest();
    const body = new FormData();

    body.append("file", file);

    xhr.upload.onprogress = progressCallback || null;

    xhr.onload = () => {
      if (xhr.status === 200) {
        resolve(xhr.responseText);
      } else {
        reject();
      }
    };
    xhr.open("POST", uploadUrl, true);
    xhr.send(body);
  });
};

export default App;

Let’s take a look at some parts of the code.

const [file, setFile] = useState<File>();
const [uploadProgress, setUploadProgress] = useState<[number, number]>([
  0,
  0
]);

We store 2 pieces of state here to store the file and the progress of the upload. The progress of the upload is represented as a 2-tuple. The first number indicates the number of bytes uploaded so far, and the second number indicates the total size of the request.

Why do we want to maintain the total size of the request here, instead of getting it from the size attribute of the file object?

The answer is because the XHR request size includes not just the file itself but also headers and other overhead. If we were to use just the size attribute from the file object, the UI would display something like 7200 of 7000 bytes uploaded when the upload is complete.


const uploadFile: (
  file: File,
  progressCallback?: (x: ProgressEvent<EventTarget>) => void
) => Promise<string> = async (file, progressCallback) => {

We define an async/await version of uploadFile that accepts a file and a progress callback function. This callback function will take in the ProgressEvent object emitted by the onprogress event.

This object has the loaded and total attributes, which we use to update the state of the upload progress here:

uploadFile(file, evt => {
  setUploadProgress([evt.loaded, evt.total]);
});

return new Promise((resolve, reject) => {
  const xhr = new XMLHttpRequest();
  const body = new FormData();

  body.append("file", file);

Here, notice that we’re using XMLHttpRequest instead of the newer fetch API. This is because fetch does not support monitoring the progress of uploads, only downloads.

    xhr.upload.onprogress = progressCallback || null;

We attach the callback function to the xhr.upload.onprogress event. Do not confuse this with the xhr.onprogress function. The latter tracks the download progress of the request.