Implementing file downloads in React applications becomes complex when dealing with authenticated APIs. While basic downloads can be handled with simple anchor tags, secure file downloads through API endpoints require careful handling of authentication, binary data, and user feedback.
This article presents a TypeScript-based custom hook solution that encapsulates these complexities into a reusable implementation, providing a clean API for components while handling the technical challenges of secure file downloads.
Let's break down the solution into two main approaches:
The simplest approach using anchor tag:
<a href="https://www.some/url/to/file" target="__blank">Download</a>
This works for public files but fails for authenticated resources:
<!-- Won't work for protected files -->
<a href="https://api.example.com/protected-file">Download</a>
Our solution involves:
First, create a React project with TypeScript and install dependencies:
npx create-react-app react-download-file-axios --template typescript
npm i axios @types/axios luxon @types/react bootstrap react-bootstrap
Our implementation consists of three main parts:
A reusable button handling loading states:
import React from "react";
import { Button as InternalButton, Spinner } from "react-bootstrap";
export enum ButtonState {
Primary = "Primary",
Loading = "Loading",
}
interface ButtonProps {
readonly buttonState: ButtonState;
readonly onClick: () => void;
readonly label: string;
}
export const Button: React.FC<ButtonProps> = ({ buttonState, onClick, label }) => {
const isLoading = buttonState === ButtonState.Loading;
return (
<div className="d-flex justify-content-center mt-5">
<InternalButton onClick={onClick} variant="primary" size="sm">
{isLoading && (
<Spinner as="span" animation="border" size="sm" role="status" aria-hidden="true" />
)}
{!isLoading && label}
</InternalButton>
</div>
);
};
Button Component accepts three props:
The core logic for file downloads:
import { AxiosResponse } from "axios";
import { useRef, useState } from "react";
interface DownloadFileProps {
readonly apiDefinition: () => Promise<AxiosResponse<Blob>>;
readonly preDownloading: () => void;
readonly postDownloading: () => void;
readonly onError: () => void;
readonly getFileName: () => string;
}
interface DownloadedFileInfo {
readonly download: () => Promise<void>;
readonly ref: React.MutableRefObject<HTMLAnchorElement | null>;
readonly name: string | undefined;
readonly url: string | undefined;
}
export const useDownloadFile = ({
apiDefinition,
preDownloading,
postDownloading,
onError,
getFileName,
}: DownloadFileProps): DownloadedFileInfo => {
const ref = useRef<HTMLAnchorElement | null>(null);
const [url, setFileUrl] = useState<string>();
const [name, setFileName] = useState<string>();
const download = async () => {
try {
preDownloading();
const { data } = await apiDefinition();
const url = URL.createObjectURL(new Blob([data]));
setFileUrl(url);
setFileName(getFileName());
ref.current?.click();
postDownloading();
URL.revokeObjectURL(url);
} catch (error) {
onError();
}
};
return { download, ref, url, name };
};
apiDefinition
Promise<AxiosResponse<Blob>>
preDownloading
postDownloading
onError
getFileName
The download process follows these exact steps:
Invoke preDownloading function
Call the API
Create URL from response
<a href...>
Generate filename
download
attribute
Click hidden anchor
Post-download cleanup
Complete example showing hook usage:
import axios from "axios";
import React, { useState } from "react";
import { DateTime } from "luxon";
import { useDownloadFile } from "../../customHooks/useDownloadFile";
import { Button, ButtonState } from "../button";
import { Alert, Container } from "react-bootstrap";
export const DownloadSampleCsvFile: React.FC = () => {
// State Management
const [buttonState, setButtonState] = useState<ButtonState>(ButtonState.Primary);
const [showAlert, setShowAlert] = useState<boolean>(false);
// Handler Functions
const preDownloading = () => setButtonState(ButtonState.Loading);
const postDownloading = () => setButtonState(ButtonState.Primary);
const onErrorDownloadFile = () => {
setButtonState(ButtonState.Primary);
setShowAlert(true);
setTimeout(() => {
setShowAlert(false);
}, 3000);
};
const getFileName = () => {
return DateTime.local().toISODate() + "_sample-file.csv";
};
const downloadSampleCsvFile = () => {
return axios.get(
"https://raw.githubusercontent.com/anubhav-goel/react-download-file-axios"
+ "/main/sampleFiles/csv-sample.csv",
{
responseType: "blob",
headers: {
Authorization: "Bearer <token>", // Add authentication token
},
},
);
};
const { ref, url, download, name } = useDownloadFile({
apiDefinition: downloadSampleCsvFile,
preDownloading,
postDownloading,
onError: onErrorDownloadFile,
getFileName,
});
return (
<Container className="mt-5">
<Alert variant="danger" show={showAlert}>
Something went wrong. Please try again!
</Alert>
<a href={url} download={name} className="hidden" ref={ref} />
<Button label="Download" buttonState={buttonState} onClick={download} />
</Container>
);
};
State Variables
buttonState
: Manages button loading stateshowAlert
: Controls error alert visibility
Hook Functions
preDownloadFile
: Sets loading statepostDownloadFile
: Resets to primary stateonError
: Shows error alertapiDefinition
: Makes authenticated API callgetFileName
: Generates timestamp filename
JSX Elements
The component returns three main JSX elements:
Hidden Anchor Tag:
<a href={url} download={name} className="hidden" ref={ref} />
This hidden tag is not visible to the user. The download function internally clicks this button after the file is downloaded using refs.
Download Button:
<Button label="Download" buttonState={buttonState} onClick={download} />
The button which users click to initiate the download.
Different Button States
Alert Component:
<Alert variant="danger" show={showAlert}>
Something went wrong. Please try again!
</Alert>
Responsible for showing error messages on the UI.
Error State Display
Finally, integrate into your App:
import React from "react";
import { DownloadSampleCsvFile } from "./components/downloadSampleCsvFile";
const App: React.FC = () => {
return <DownloadSampleCsvFile />;
};
export default App;
npm start
# or
yarn start
This implementation provides a reusable solution for handling authenticated file downloads in React applications. The custom hook pattern encapsulates complexity while maintaining type safety and proper resource management.
For complete source code, visit the GitHub repository.