React file download with progress bar

React file download with progress bar

·

8 min read

React is a popular JavaScript library used for building user interfaces. One common requirement in web applications is the ability to download files. In this blog, we will explore how to download files in React with a progress bar.

Prerequisites

Before we get started, ensure that you have the following prerequisites:

  • Basic knowledge of HTML, CSS, and JavaScript

  • Node.js and npm installed on your system

  • A text editor like Visual Studio Code

Getting started

Let's create a new React.js project by running the following command in your terminal:

npx create-react-app react-file-download-with-progress-bar

This command creates a new React.js project with the name react-file-download-with-progress-bar. Navigate to the project directory by running the following command:

cd react-file-download-with-progress-bar

Open the project directory in your text editor. Next, install the axios and react-toastify library by running the following command:

npm install axios react-toastify

This command installs the latest version of the axios and react-toastify library in your project.

Creating the file downloader component

Create a new file named FileDownloaderProgressBar.tsx in the src/features directory of your project. This file will contain the code for the file downloader component. Here's the code for the component:

import axios from 'axios';
import { memo, useEffect, useRef, useState } from 'react';
import { toast } from 'react-toastify';

import Icon from 'components/Icon';
import { bytesToMB } from 'utils/helpers';

interface FileDownloaderProgressBarProps {
  downloadQueueFiles: string[];
  setDownloadQueueFiles: Function;
}

interface SingleItemDownloadProps {
  setDownloadQueueFiles: Function;
  downloadUrl: string;
}

const FileDownloaderProgressBar = (props: FileDownloaderProgressBarProps) => {
  const { downloadQueueFiles, setDownloadQueueFiles } = props;

  if (!downloadQueueFiles?.length) return null;

  return (
    <div className="absolute right-0 bottom-0 w-auto mr-2 bg-gray-300 p-2 z-50 rounded-sm">
      <div className="w-full text-xs py-1">
        {downloadQueueFiles?.map((downloadUrl: string, index: number) => (
          <SingleItemDownload key={index} setDownloadQueueFiles={setDownloadQueueFiles} downloadUrl={downloadUrl} />
        ))}
      </div>
    </div>
  );
};

const SingleItemDownload = ({ setDownloadQueueFiles, downloadUrl }: SingleItemDownloadProps) => {
  const isDownloadingStartRef = useRef(true);

  const [progressInfo, setProgressInfo] = useState<any>({
    fileName: downloadUrl.split('/').pop(),
    progress: 0,
    isCompleted: false,
    total: 0,
    loaded: 0,
  });

  const CancelToken: any = axios.CancelToken;
  const cancelSource: any = useRef<any>(null);

  useEffect(() => {
    if (!!isDownloadingStartRef?.current) {
      const startDownload = async () => {
        cancelSource.current = CancelToken.source();

        await axios
          .get(downloadUrl, {
            responseType: 'blob',
            onDownloadProgress: (progressEvent: any) => {
              const { loaded, total } = progressEvent;
              const progress = Math.floor((loaded * 100) / total);
              setProgressInfo((info: any) => ({ ...info, progress, loaded, total }));
            },
            cancelToken: cancelSource.current.token,
          })
          .then(function (response) {
            const url = window.URL.createObjectURL(
              new Blob([response.data], {
                type: response.headers['content-type'],
              })
            );

            const link = document.createElement('a');
            link.href = url;
            link.setAttribute('download', progressInfo?.fileName);
            document.body.appendChild(link);
            link.click();
            document.body.removeChild(link);
            URL.revokeObjectURL(url);

            setProgressInfo((info: any) => ({ ...info, isCompleted: true }));
            toast.success(`${progressInfo?.fileName} successfully downloaded.`);
          })
          .catch((err: any) => {
            setProgressInfo((info: any) => ({ ...info, isCanceled: true }));
            toast.error(err.message);
          })
          .finally(() => {
            setDownloadQueueFiles((prevFiles: string[]) => prevFiles?.filter((url) => url !== downloadUrl));
          });
      };

      startDownload();
    }

    return () => {
      isDownloadingStartRef.current = false;
    };
  }, []);

  const onDownloadCancel = () => {
    cancelSource.current.cancel(`${progressInfo?.fileName} downloading cancelled.`);
  };

  return (
    <div className="flex justify-between items-center space-x-2">
      <span>
        <Icon name="photo" className="w-6 h-6 text-white" />
      </span>
      <span>
        {!progressInfo?.isCompleted && progressInfo?.loaded <= 0 ? (
          <span>{progressInfo?.fileName} waiting for download.</span>
        ) : (
          <span className="flex space-x-1 justify-start items-center">
            {progressInfo?.fileName} - (
            <span>
              <span className="text-red-600">{bytesToMB(progressInfo.loaded)}</span> of {bytesToMB(progressInfo.total)}
              MB)
            </span>
            {!progressInfo?.isCompleted && progressInfo?.progress < 100 && (
              <button onClick={onDownloadCancel}>
                <Icon name="close" className="w-4 h-4 rounded-full border border-red-600 text-red-600" />
              </button>
            )}
          </span>
        )}
      </span>
    </div>
  );
};

export default memo(FileDownloaderProgressBar);

The above code exports a React component FileDownloaderProgressBar and an interface FileDownloaderProgressBarProps. The component displays a list of files that can be downloaded and allows the user to download them one at a time.

The SingleItemDownload component is a child component of FileDownloaderProgressBar and it handles the download of a single file. It uses Axios to download a file and updates the download progress as a percentage. If the download is canceled, the component shows an error message.

The useEffect hook in SingleItemDownload is used to start the download when the component is mounted, and to clean up the component when it is unmounted. The component uses useState hook to manage the download progress state and useRef to store the current state of downloading.

react-toastify is used to display success and error messages in the browser. Icon and bytesToMB are utility components that render an icon and convert bytes to MB respectively.

Let’s import the FileDownloaderProgressBar component in the app.tsx or other components and pass the props. Here's the code for the app.tsx

import { useState } from 'react';
import { toast } from 'react-toastify';

import Icon from 'components/Icon';
import FileDownloaderProgressBar from './FileDownloaderProgressBar';

const files: string[] = [
  'https://images.unsplash.com/photo-1604264849633-67b1ea2ce0a4',
  'https://images.unsplash.com/photo-1628856860837-208e8e66e288',
  'https://images.unsplash.com/photo-1604263439201-171fb8c0fddc',
  'https://images.unsplash.com/photo-1607205854688-e2d8654b0c2e',
];

const Feature = () => {
  const [downloadQueueFiles, setDownloadQueueFiles] = useState<string[]>([]);

  const download = async (newUrl: string) => {
    if (newUrl) {
      if (!!downloadQueueFiles?.filter((url) => url === newUrl).length) {
        toast.error('This file already downloading.');
        return false;
      }

      setDownloadQueueFiles((prevFiles) => [...prevFiles, newUrl]);
    }
  };

  return (
    <div className="relative px-5 py-2">
      <div className="flex justify-center">
        <div className="w-[50vw] h-full">
          <h2 className="flex justify-between items-center font-semibold text-black mb-2">Download Files</h2>
          <div className="flex w-full h-[75vh] justify-center items-start border border-dashed border-black text-white p-4">
            <div className="w-full text-black grid grid-cols-4">
              {!!files?.length &&
                files?.map((url: string, index: number) => (
                  <>
                    <div className="w-36">
                      <button
                        onClick={() => download(url)}
                        className="flex justify-center items-center space-x-2 bg-yellow-300 px-2 py-1 rounded mt-1"
                      >
                        <Icon name="cloud-download" className="w-6 h-6" /> <span>File - {index + 1}</span>
                      </button>
                    </div>
                  </>
                ))}
            </div>
          </div>
        </div>
      </div>
      <FileDownloaderProgressBar
        downloadQueueFiles={downloadQueueFiles}
        setDownloadQueueFiles={setDownloadQueueFiles}
      />
    </div>
  );
};

export default Feature;

The above code defines a React functional component named Feature that imports the useState hook and toast and Icon components from the react-toastify and components modules respectively. It also imports a local component called FileDownloaderProgressBar.

The component defines an array of URLs as files and initializes a state variable downloadQueueFiles using the useState hook.

The download function is an asynchronous function that takes a URL as an argument, checks if it is a new URL and not already downloading. If it is not already downloading, it sets the URL in the downloadQueueFiles state array.

The component returns a div element that contains a list of download buttons that download the files when clicked. Each button calls the download function with a URL passed as an argument. If the file is already downloading, an error message is shown. Additionally, the component renders the FileDownloaderProgressBar component, which shows the progress of the downloads.

Summary

In this blog post, we cover how to implement a file download progress bar in a React application using Axios and the useState and useEffect hooks. Specifically, we show how to download a file from a server and display a progress bar that updates as the download progresses.

By the end of this blog post, readers should have a good understanding of how to implement a file download progress bar in a React application and be able to apply this knowledge to their own projects. This can be useful for applications that need to download large files or provide a better user experience for file downloads.

I hope you found this tutorial helpful! If you have any questions or feedback, feel free to let me know.

Download full source code from GitHub