Having a progress bar indicator on your website will help your visitors know where they're headed, especially when dealing with slow internet or large files.

The purpose of this article is to have some type of indicator of how the upload is progressing and how much time remains before the upload is complete.

In this article, you will learn how to get the upload progress information using the popular package Axios as well as how to use this information to calculate the percentage and remaining duration.

If you're considering following along itโ€™s very important to read the article where we built a file uploader from scratch with Next.js and formidable as this one is based on it.

Brief summary

As mentioned before, we will use a previous article as a base for this one, to avoid duplication of unnecessary content.

We used fetch api to upload a file to the api endpoint that we have created using formidable to parse our form.

From what I see, the fetch api doesn't support listening to events that can help us track the request progress, and to solve this problem we choose to use Axios as an option.

We could have chosen to use the XMLHttpRequest API as well but Axios seems a popular and solid package that takes a lot of overhead away.

Setup the project

First, we need to clone the file uploader Next.js repository that we created in the other articles. If you already have that you can skip this step.

Open your terminal, navigate to where you want to clone the project, and run the command: git clone https://github.com/codersteps/nextjs_file_uploader.git nextjs_file_uploader_progress_bar.

This will create a new folder nextjs_file_uploader_progress_bar and open it using your preferred code editor for me it's vs code.

Now navigate to the project root cd nextjs_file_uploader_progress_bar and install the NPM dependencies npm install after that start the dev server npm run dev.

You can now open http://localhost:3000/ to access the file uploader website.

Refactor Fetch api to Axios

Currently, we are using the Fetch api to send the file to the server, our first step is to achieve the same functionality with Axios instead.

Let's install it with npm install axios this will install the Axios package which comes with its own types so no need to install them.

Now open pages/index.tsx and locate the onUploadFile function where we call the /api/upload endpoint with the selected file.

At this point, we won't do a lot of changes and it should feel simpler than using fetch as axios have a very nice api.

pages/index.tsx
TypeScript
// imports
import axios, { AxiosRequestConfig } from "axios";

// replace the old one with this one
const onUploadFile = async (e: MouseEvent<HTMLButtonElement>) => {
  e.preventDefault();

  if (!file) {
    return;
  }

  try {
    let formData = new FormData();
    formData.append("media", file);

    const options: AxiosRequestConfig = {
      headers: { "Content-Type": "multipart/form-data" },
    };

    const {
      data: { data },
    } = await axios.post<{
      data: {
        url: string | string[];
      };
    }>("/api/upload", formData, options);

    console.log("File was uploaded successfylly:", data);
  } catch (e: any) {
    console.error(e);
    const error =
      e.response && e.response.data
        ? e.response.data.error
        : "Sorry! something went wrong.";
    alert(error);
  }
};

Axios throw an error when the response doesn't have a success status code, in our case, it's 400 or 500 so we handle the response error inside the catch block.

This is the same as it was before we just changed from Fetch to Axios, first we import the package at the top, then we update the function onUploadFile to use axios.

We keep the formData the same as before, then we create an options object for Axios with headers to tell the server that we are sending a multipart form data which means we are dealing files.

After that, we call the upload api with a post request axios.post sending the formData as a second argument and the options as a third argument.

The axios call returns a response object, this response contains a data property which itself contains the data we sent back from the upload api.

We destruct the data that contains the uploaded file path from the response, we don't need the error here, as we will use it later when something goes wrong.

You maybe noticed that we passed a response data type to the axsio.post<T> as generic, this is a way to tell axios what we expect as a response.

Finally, we do some exception handling, this can happen if the file is invalid, if there was a network problem or maybe something went wrong.

We check if there's a response object (API error), then if the response has any data, if so we extract the error message sent back from the api.

If there was no error message on the response we just use a general message "Sorry! something went wrong." you can handle these errors as you see fit and adapt them to your situation.

Add progress bar design

We will have a simple progress bar under our file uploader, for that, we will create a new component called SimpleProgressBar, then we will add it under the upload form.

Lets create a new file at components/common/SimpleProgressBar.tsx then match the component content as follow:

components/common/SimpleProgressBar.tsx
TypeScript
const SimpleProgressBar = ({ progress = 0 }: { progress?: number }) => {
  return (
    <div className="py-1.5 h-6 relative">
      <div className="absolute top-0 bottom-0 left-0 w-full h-full bg-gray-400"></div>
      <div
        style={{
          width: `${progress}%`,
        }}
        className="absolute top-0 bottom-0 left-0 h-full transition-all duration-150 bg-gray-600"
      ></div>
      <div className="absolute top-0 bottom-0 left-0 flex items-center justify-center w-full h-full">
        <span className="text-xs font-bold text-white">{progress}%</span>
      </div>
    </div>
  );
};

export default SimpleProgressBar;

It's not that complicated to understand, but let's take it step-by-step, we created the component function then we export it at the bottom for other components to use.

We have a wrapper with position: relative and a static height, we do this to correctly position the three div elements inside.

All the div elements have a position of absolute and they're on top of each other with the same height and same position.

The first element is used as a background of the trajectory, the second element is used for the progress bar and has a more saturated background, and the third is for showing the percentage number.

The SimpleProgressBar component has a single property which is the current progress, it's used on the percentage text, and on the progress bar width.

If you already using a design system that has its own progress bar, you may want to use that instead in order to stay consistent with your design.

File upload progress percentage

Axios can take onUploadProgress and _ onDownloadProgress_ as part of the options object, which takes as a value a callback to handle the native progress event.

You guessed it we are going to use the onUploadProgress option to get the request progress information while uploading which will allow us to calculate the progress percentage.

pages/index.tsx
TypeScript
// Declare a new state to store the progress
const [progress, setProgress] = useState(0);

// Add the onUploadProgress option
const options: AxiosRequestConfig = {
  headers: { "Content-Type": "multipart/form-data" },
  onUploadProgress: (progressEvent: any) => {
    const percentage = (progressEvent.loaded * 100) / progressEvent.total;
    setProgress(+percentage.toFixed(2));
  },
};

// Pass the progress state to the SimpleProgressBar component
<SimpleProgressBar progress={progress} />

We created a new state to store the progress which we do pass to the SimpleProgressBar component at the bottom after the upload form instead of a static value.

When the onUploadProgress gets called we get a progressEvent with two important properties (ProgressEvent | MDN).

loaded: the uploaded bytes that have already been sent to the server.

total: the total bytes that need to be sent to the server in order for the upload to be completed.

It's easy to calculate the progress percentage when you have the loaded and total values, it's a simple math operation.

It's like this: we have the total is 100% and the loaded is X% to calculate the X we do Xtotal = loaded100 and finally, X is loaded*100/total.

After doing the changes, you can now run the development server, select a file then upload it you should see that progress bar progressing.

When developing locally it's too fast for you to see the progress bar in action, so you need to change the network to "Slow 3G" to simulate a slow internet (Need some help with that?).

File upload remaining duration

After calculating the progress percentage, we have now the setup to easily calculate the remaining duration for the upload to complete.

Update your code to match the changes we've done, then we'll go through each change.

pages/index.tsx
TypeScript
// Declare a new state to store the remaining
const [remaining, setRemaining] = useState(0);

// Find this part of code and do the needed changes
try {
  let startAt = Date.now(); // To keep track of the upload start time
  let formData = new FormData();
  formData.append("media", file);

  const options: AxiosRequestConfig = {
    headers: { "Content-Type": "multipart/form-data" },
    onUploadProgress: (progressEvent: any) => {
      const { loaded, total } = progressEvent;

      // Calculate the progress percentage
      const percentage = (loaded * 100) / total;
      setProgress(+percentage.toFixed(2));

      // Calculate the progress duration
      const timeElapsed = Date.now() - startAt;
      const uploadSpeed = loaded / timeElapsed;
      const duration = (total - loaded) / uploadSpeed;
      setRemaining(duration);
    },
  };

  // The rest of the code
} catch (e) {
  // previous code
}

// At the bottom
<SimpleProgressBar progress={progress} remaining={remaining} />

We created a new state to store the remaining time which we do pass to the SimpleProgressBar component at the bottom as a new property that we'll cover next.

We declared a new variable to store when the upload started, we use this one to calculate the time that has been passed since the upload started we then store it in the timeLapsed variable.

We did some refactoring by destructuring the loaded and total properties from the progressEvent object.

With the time elapsed calculated we use it to calculate the speed of the upload, as a result of dividing the loaded by the timeElapsed

We kind of see how much it took to upload the loaded bytes with the current upload speed then we take that and calculate the time needed to upload the rest of the file by subtracting loaded from _total.

Add remaining property to SimpleProgressBar

We added a new property to the SimpleProgressBar component to display the remaining time value because the remaining time is in milliseconds we used the pretty-ms NPM package to convert from milliseconds to a human-readable format.

First, install the pretty-ms package with npm install pretty-ms then do the needed changes to the SimpleProgressBar component.

components/common/SimpleProgressBar.tsx
TypeScript
import prettyMS from "pretty-ms";

const SimpleProgressBar = ({
  progress = 0,
  remaining = 0,
}: {
  progress?: number;
  remaining?: number;
}) => {
  return (
    <>
      {!!remaining && (
        <div className="mb-1.5 text-sm text-gray-700">
          Remaining time: {prettyMS(remaining)}
        </div>
      )}
      <div className="py-1.5 h-6 relative">
        {/* The rest of the JSX */}
      </div>
    </>
  );
};

export default SimpleProgressBar;

The changes here are simple, we added a new property for the remaining time then we used it to show the remaining time with the help of the pretty-ms package.

After you've completed all the changes, you should be able to see both the progress bar and the remaining time changing relative to the file size and the upload speed.

Conclusion

In this article, we have used a previous article as a base for the file uploader used for the demonstration.

We successfully added progress indicators using a progress bar with percentage value and the remaining time to upload.

Source code is available at: https://github.com/codersteps/nextjs_file_uploader_progress_bar.

I hope this was helpful for you, see you in the next one ๐Ÿ˜‰.