

Abstract
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.
Solution Details
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>
2. API-Based Solution
Our solution involves:
- Calling authenticated API
- Handling blob response
- Creating temporary URL
- Triggering download programmatically
Implementation Details
1. Project Setup
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
2. Component Structure
Our implementation consists of three main parts:
- Button Component (UI + Loading States)
- Custom Download Hook (Core Logic)
- Implementation Component (Usage Example)
Button Component
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:
- buttonState: Controls loading/normal state
- onClick: Handler function for click events
- label: Text displayed on the button
Custom Download Hook
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 };
};
Hook Parameters Explained
-
apiDefinition
- Function containing API call
- Returns
Promise<AxiosResponse<Blob>>
- Includes authentication headers
-
preDownloading
- Executed before API call
- Used for UI state changes
- Example: Setting button to loading
-
postDownloading
- Executed after successful download
- Resets UI states
- Example: Setting button back to primary
-
onError
- Called when API fails
- Handles error scenarios
- Example: Shows error alert
-
getFileName
- Generates download filename
- Called after successful download
- Example: Creates timestamp-based name
Download Function Flow
The download process follows these exact steps:
-
Invoke preDownloading function
- Triggers loading state
- Prepares UI for download
-
Call the API
- Makes authenticated request
- Handles blob response
- Triggers onError if fails
-
Create URL from response
- Creates blob URL
- Stores in state
- Used in
<a href...>
-
Generate filename
- Creates download filename
- Stores in state
- Used in
download
attribute
-
Click hidden anchor
- Uses ref to access DOM
- Triggers actual download
-
Post-download cleanup
- Calls postDownloading
- Resets UI state
- Destroys generated URL
Implementation Component
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>
);
};
Component Breakdown
-
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.
Normal Button State Loading Button State 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
App Integration
Finally, integrate into your App:
import React from "react";
import { DownloadSampleCsvFile } from "./components/downloadSampleCsvFile";
const App: React.FC = () => {
return <DownloadSampleCsvFile />;
};
export default App;


Testing
- Start the development server:
npm start
# or
yarn start
- Test different scenarios:
- Normal download flow
- Loading state display
- Error handling
- Authentication
- File naming
Conclusion
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.