Skip to content

useDownload

React hook to download a file.

Add the hook via the CLI:

sh
npx @novajslabs/cli add useDownload
sh
npx @novajslabs/cli add useDownload
sh
pnpm dlx @novajslabs/cli add useDownload

Or copy and paste the code into your project:

ts
import { useState } from "react";

export const useDownload = () => {
  const [error, setError] = useState<Error | unknown | null>(null);
  const [isDownloading, setIsDownloading] = useState<boolean>(false);
  const [progress, setProgress] = useState<number | null>(null);

  const handleResponse = async (response: Response): Promise<string> => {
    if (!response.ok) {
      throw new Error("Could not download file");
    }

    const contentLength = response.headers.get("content-length");
    const reader = response.clone().body?.getReader();

    if (!contentLength || !reader) {
      const blob = await response.blob();

      return createBlobURL(blob);
    }

    const stream = await getStream(contentLength, reader);
    const newResponse = new Response(stream);
    const blob = await newResponse.blob();

    return createBlobURL(blob);
  };

  const getStream = async (
    contentLength: string,
    reader: ReadableStreamDefaultReader<Uint8Array>
  ): Promise<ReadableStream<Uint8Array>> => {
    let loaded = 0;
    const total = parseInt(contentLength, 10);

    return new ReadableStream<Uint8Array>({
      async start(controller) {
        try {
          for (;;) {
            const { done, value } = await reader.read();

            if (done) break;

            loaded += value.byteLength;
            const percentage = Math.trunc((loaded / total) * 100);
            setProgress(percentage);
            controller.enqueue(value);
          }
        } catch (error) {
          controller.error(error);
          throw error;
        } finally {
          controller.close();
        }
      },
    });
  };

  const createBlobURL = (blob: Blob): string => {
    return window.URL.createObjectURL(blob);
  };

  const handleDownload = (fileName: string, url: string) => {
    const link = document.createElement("a");

    link.href = url;
    link.setAttribute("download", fileName);
    document.body.appendChild(link);
    link.click();
    document.body.removeChild(link);
    window.URL.revokeObjectURL(url);
  };

  const downloadFile = async (fileName: string, fileUrl: string) => {
    setIsDownloading(true);
    setError(null);
    setProgress(null);

    try {
      const response = await fetch(fileUrl);
      const url = await handleResponse(response);

      handleDownload(fileName, url);
    } catch (error) {
      setError(error);
    } finally {
      setIsDownloading(false);
    }
  };

  return {
    error,
    isDownloading,
    progress,
    downloadFile,
  };
};
js
import { useState } from "react";

export const useDownload = () => {
  const [error, setError] = useState(null);
  const [isDownloading, setIsDownloading] = useState(false);
  const [progress, setProgress] = useState(null);

  const handleResponse = async (response) => {
    if (!response.ok) {
      throw new Error("Could not download file");
    }

    const contentLength = response.headers.get("content-length");
    const reader = response.clone().body?.getReader();

    if (!contentLength || !reader) {
      const blob = await response.blob();

      return createBlobURL(blob);
    }

    const stream = await getStream(contentLength, reader);
    const newResponse = new Response(stream);
    const blob = await newResponse.blob();

    return createBlobURL(blob);
  };

  const getStream = async (contentLength, reader) => {
    let loaded = 0;
    const total = parseInt(contentLength, 10);

    return new ReadableStream({
      async start(controller) {
        try {
          for (;;) {
            const { done, value } = await reader.read();

            if (done) break;

            loaded += value.byteLength;
            const percentage = Math.trunc((loaded / total) * 100);
            setProgress(percentage);
            controller.enqueue(value);
          }
        } catch (error) {
          controller.error(error);
          throw error;
        } finally {
          controller.close();
        }
      },
    });
  };

  const createBlobURL = (blob) => {
    return window.URL.createObjectURL(blob);
  };

  const handleDownload = (fileName, url) => {
    const link = document.createElement("a");

    link.href = url;
    link.setAttribute("download", fileName);
    document.body.appendChild(link);
    link.click();
    document.body.removeChild(link);
    window.URL.revokeObjectURL(url);
  };

  const downloadFile = async (fileName, fileUrl) => {
    setIsDownloading(true);
    setError(null);
    setProgress(null);

    try {
      const response = await fetch(fileUrl);
      const url = await handleResponse(response);

      handleDownload(fileName, url);
    } catch (error) {
      setError(error);
    } finally {
      setIsDownloading(false);
    }
  };

  return {
    error,
    isDownloading,
    progress,
    downloadFile,
  };
};

Requirements

React 16.8 or higher

Return values

error

Type: Error | unknown | null

Represents any error that occurs during the download process, or null if no error occurs.

isDownloading

Type: boolean

Indicates whether a file is currently being downloaded (true) or not (false).

downloadFile

Type: function

Initiate the file download process. Accepts two parameters: fileName, a string that represents the name to be given to the downloaded file, and fileUrl, a string that represents the URL from which the file is to be downloaded.

progress

Type: number | null

Indicates the percentage of download progress.

Example

tsx
import { useDownload } from "./hooks/useDownload";

const App = () => {
  const { error, isDownloading, downloadFile } = useDownload();

  const handleDownload = () => {
    const fileName = "example.pdf";
    const fileUrl = "https://example.com/example.pdf";
    downloadFile(fileName, fileUrl);
  };

  return (
    <div>
      <button onClick={handleDownload} disabled={isDownloading}>
        {isDownloading ? "Downloading..." : "Download File"}
      </button>
      {error && <div>Error: {error.message}</div>}
    </div>
  );
};

export default App;

Use cases

Here are some use cases where this React hook is useful:

  • Downloading PDF invoices in an e-commerce application
  • Exporting data as a CSV file in a data visualization dashboard
  • Providing downloadable resources (e.g., whitepapers, guides) on a website