Download Multiple Files as a ZIP in React
Build a production "Download all" button with @eazip/react — fetch the file list, start the job with useEazip(), and let <EazipTray /> handle progress, partial results, and retry.
This guide builds the complete "Download all" feature in React: a button that turns any list of files — picker uploads, URLs from state, or a signed list from your API — into one ZIP download, with progress, cancel, partial results, and retry handled for you.
Here is the result — try it:
Loading live demo…
First time rendering Eazip.js? Install and first ZIP live in
Getting Started / React; this page
assumes @eazip/react is installed and builds the real feature.
The complete component
Two pieces: a button that starts the task, and one tray near the app root that presents it.
import { useEazip } from '@eazip/react';
export function DownloadAllButton() {
const zip = useEazip();
async function handleDownloadAll() {
const response = await fetch('/api/exports/files', {
credentials: 'include',
});
if (!response.ok) {
throw new Error('Could not prepare export files');
}
const files: { url: string; filename: string }[] = await response.json();
zip.download({ files, zipName: 'export.zip' });
}
return (
<button disabled={zip.isBusy} onClick={handleDownloadAll}>
Download all
</button>
);
}import { EazipTray } from '@eazip/react';
export default function App() {
return (
<>
<Routes />
<EazipTray />
</>
);
}download() starts the task and returns immediately; from that moment the
tray takes over — progress, cancel, the finished download, skipped-file
notices, and retry. disabled={zip.isBusy} keeps a second click from
stacking a job on top of a running one, and an empty files list fails
synchronously, so keep the button disabled while there is nothing to
export.
Where the file list comes from
files accepts browser files, URL strings, and named entries, mixed
freely in one array:
- From a picker or drag-and-drop: pass the
File[](orFileList) straight from your state — no server involved. - URLs the component already has: pass the strings as-is. Remote URLs fetched in the browser must allow cross-origin requests — see Inputs and sources.
- From your API (private storage): the usual production shape — your endpoint authorizes the user and returns short-lived signed URLs, as in the component above. The per-platform signing code is in the storage guides, for example Download an S3 Bucket as a ZIP in the Browser.
Name the entries — and the folders
filename controls what the person sees after unzipping, and forward
slashes create folders inside the archive:
zip.download({
files: [
{ url: signed.invoice, filename: 'invoices/2026-08.pdf' },
{ url: signed.photo, filename: 'photos/cover.jpg' },
{ file: localNotes, filename: 'notes.txt' },
],
zipName: 'my-account-export.zip',
});Raw storage keys make bad filenames; rename on the way in.
The states your users will actually hit
The tray presents all of these already — this table is for deciding what, if anything, you want to customize:
| Task state | What happened | The tray shows |
|---|---|---|
processing | Files are being fetched and packaged | Progress and cancel |
completed | The ZIP is ready | The download action |
partial | Some files failed; the rest shipped | Download plus a skipped-count notice |
failed | No usable output | The error, and retry when canRetry is true |
partial is the state worth designing for: one expired URL out of forty
should not cost the user the other thirty-nine, and Eazip.js ships the
usable archive by default — see
Partial results and errors. If the
tray's presentation doesn't fit your design system, the same task state
drives a fully custom interface through useEazip() — see
Headless usage.
When browser limits bite
A Local job runs in the tab: memory holds the archive and the job ends if the tab closes. When exports reach multiple gigabytes, thousands of URLs, or must survive a reload, the same call moves to managed Eazip by adding two options:
zip.download({
strategy: 'cloud',
publicKey: 'pk_ez_...',
files,
zipName: 'export.zip',
});The button, tray, and task states stay identical. See Scale with Eazip for the boundary, and Choose your integration when the URL list itself is too large to send to the browser.
FAQ
Can I mix local files and remote URLs in one ZIP?
Yes — one files array takes File/Blob objects, URL strings, and
named entries together. Each entry is fetched or read as appropriate and
lands in the same archive.
How do I show progress in my own UI instead of the tray?
Read the current task from useEazip() and render from its state — the
React Reference documents the exact task
fields, and Headless usage shows a complete
custom interface.
Why did my export fail with CORS errors on some files?
Those URLs' hosts don't allow browser requests from your origin. Signed URLs from your own storage need a bucket CORS rule (each storage guide shows it); third-party URLs you don't control may simply not be fetchable from a browser — see Inputs and sources.
What happens if the user navigates away mid-download?
A Local job lives in the page; a full navigation ends it. Within a SPA,
route changes are fine — keep <EazipTray /> mounted at the root so the
task presentation survives them. For jobs that must outlive the tab, use
the managed path above.
Do I need EazipProvider?
Not for this feature. Add it when several components share defaults (a
common zipName, a Cloud key) or when tests need isolated state.
Download Multiple Files at Once in JavaScript
Why the loop of anchor clicks fails, what browsers actually allow, and the reliable pattern — packaging the files into one ZIP download client-side.
Let Users Download Selected Files as a ZIP
Wire checkboxes to a ZIP download — track a selection set, build the files array from it, and disable the button when nothing is selected.