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.