Skip to content

Tracking Upload Progress in Browsers

Posted on:May 10, 2020

A short piece of skeleton code to demonstrate how to track upload progress in browsers. I’ve always had the impression that most uploading/downloading progress bars were faked or had non-trivial implementations involving sockets, but it turns out you can actually do this out of the box with the Fetch API (download progress only, more on this below) or XHR (both upload and download progress).

import React, { useState } from "react";
 
const uploadUrl = "https://api.mysite.com/upload";
 
function App() {
  // highlight-start
  const [file, setFile] = useState<File>();
  const [uploadProgress, setUploadProgress] = useState<[number, number]>([
    0,
    0
  ]);
  // highlight-end
  return (
    <div className="App">
      <div>
        <button
          onClick={() => {
            if (file) {
              // highlight-start
              uploadFile(file, evt => {
                setUploadProgress([evt.loaded, evt.total]);
              });
              // highlight-end
            }
          }}
        >
          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>
  );
}
 
// highlight-start
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);
    // highlight-end
 
    // highlight-start
    xhr.upload.onprogress = progressCallback || null;
    // highlight-end
 
    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. This issue is also tracked here.

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.