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.