---
title: "Saving & downloading — React"
description: "Get the PDF with every change, let the user download or print it, keep only the changes, and warn before unsaved changes are lost."
framework: "React"
source: "https://www.cloudpdf.com/docs/headless/react/documents/saving"
---

# Saving & downloading

Everything people do in the viewer, such as annotations, form values and page edits, is part of the
document. `download()` gives you the PDF with all of it, as bytes, and `saveFile()` hands them to
the browser as a download:

**`download-button.tsx`**

```tsx
import { saveFile, useDocuments } from '@embedpdf/react/runtime';

export function DownloadButton() {
  const documents = useDocuments();
  return (
    <button onClick={async () => saveFile(await documents.download(), 'contract.pdf')}>
      Download
    </button>
  );
}
```

`download()` waits for changes still on their way, such as the last words someone typed in a text
box, so the file has everything the user sees. Without an id it downloads the document in scope,
usually the active one; `documents.download(id)` picks another.

## Send it to your server

The bytes are a normal PDF. Upload them like any file:

```ts
const bytes = await documents.download();
await fetch(`/api/files/${fileId}`, { method: 'PUT', body: bytes });
```

## A quick save or a full rewrite

```ts
await documents.download(); // appends the changes to the original file
await documents.download(undefined, { mode: 'rewrite' }); // writes a fresh, compact file
```

The default, `'incremental'`, keeps the original bytes and adds the changes after them: it's fast,
and it keeps existing digital signatures valid. `'rewrite'` writes a new file without old
revisions; use it after [redaction](../annotations/redaction), so removed content can't be recovered
from an earlier revision. See [Saving](https://www.cloudpdf.com/docs/engine/documents/saving).

**`download.tsx`**

```tsx
import { useEffect, useState } from 'react';
import { Viewer, DocumentGate, saveFile, useDocuments } from '@embedpdf/react/runtime';
import type { OpenInput, PdfSaveMode } from '@embedpdf/react/runtime';
import { Stage, stagePlugin } from '@embedpdf/react/stage';
import { RenderLayer, renderPlugin } from '@embedpdf/react/render';
import { cloudEngine } from '@cloudpdf/engine';

import './download.css';

const engine = cloudEngine();
const plugins = [stagePlugin(), renderPlugin()];

const ebook: OpenInput = { kind: 'share', shareToken: 'shr_WGj1goAtlNN_fQ5OswPrbJQM' };

const megabytes = (bytes: Uint8Array) => `${(bytes.byteLength / 1_000_000).toFixed(2)} MB`;

function DownloadBar() {
  const documents = useDocuments();
  const [mode, setMode] = useState<PdfSaveMode>('incremental');
  const [sizes, setSizes] = useState<Record<PdfSaveMode, string> | null>(null);

  // How big each kind of download is: the same document, written two ways.
  useEffect(() => {
    const cancel = new AbortController();
    const { signal } = cancel;
    Promise.all([
      documents.download(undefined, { signal }),
      documents.download(undefined, { mode: 'rewrite', signal }),
    ])
      .then(([incremental, rewrite]) =>
        setSizes({ incremental: megabytes(incremental), rewrite: megabytes(rewrite) }),
      )
      .catch(() => {}); // cancelled: the example went away
    return () => cancel.abort();
  }, [documents]);

  const download = async () => saveFile(await documents.download(undefined, { mode }), 'ebook.pdf');

  return (
    <div className="toolbar">
      <div className="segmented" role="group" aria-label="Kind of download">
        <button
          type="button"
          aria-pressed={mode === 'incremental'}
          onClick={() => setMode('incremental')}
        >
          Incremental
        </button>
        <button type="button" aria-pressed={mode === 'rewrite'} onClick={() => setMode('rewrite')}>
          Rewrite
        </button>
      </div>
      <button
        type="button"
        className="download"
        disabled={!documents.canDownload()}
        onClick={download}
      >
        Download
      </button>
      <output className="sizes">
        {sizes ? (
          <>
            Incremental <strong>{sizes.incremental}</strong> · Rewrite{' '}
            <strong>{sizes.rewrite}</strong>
          </>
        ) : (
          'Measuring…'
        )}
      </output>
    </div>
  );
}

export default function App() {
  return (
    <Viewer
      engine={engine}
      plugins={plugins}
      initialDocuments={[{ source: ebook, name: 'ebook.pdf' }]}
    >
      <DocumentGate fallback={<p className="loading">Opening…</p>}>
        <DownloadBar />
        <Stage className="stage">{() => <RenderLayer />}</Stage>
      </DocumentGate>
    </Viewer>
  );
}
```

**`download.css`**

```css
/* Colors */
:root {
  --ink: #1a2748;
  --muted: #5a6b92;
  --line: #e6eaf2;
  --line-strong: #dce8fc;
  --surface: #ffffff;
  --tint: #f3f7fe;
  --canvas: #f7f9fc;
  --focus: #7db6ff;
  --accent: #0876fd;
  --shadow: rgb(7 32 76 / 0.04);
}

/* The download, and how big each kind of file is */
.toolbar {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 8px;
  margin-bottom: 14px;
}

.segmented {
  display: inline-flex;
  padding: 3px;
  border: 1px solid var(--line);
  border-radius: 10px;
  background: var(--surface);
}

.segmented button {
  font: inherit;
  font-size: 13px;
  font-weight: 550;
  color: var(--muted);
  background: none;
  border: 0;
  border-radius: 7px;
  padding: 6px 10px;
  cursor: pointer;
}

.segmented button[aria-pressed='true'] {
  color: var(--ink);
  background: var(--tint);
}

.download {
  font: inherit;
  font-size: 13px;
  font-weight: 600;
  color: #ffffff;
  background: var(--accent);
  border: 1px solid var(--accent);
  border-radius: 9px;
  padding: 8px 14px;
  cursor: pointer;
}

.download:disabled {
  opacity: 0.45;
  cursor: not-allowed;
}

.segmented button:focus-visible,
.download:focus-visible {
  outline: 2px solid var(--focus);
  outline-offset: 2px;
}

.sizes {
  margin-left: auto;
  font-size: 13px;
  color: var(--muted);
  font-variant-numeric: tabular-nums;
}

.sizes strong {
  color: var(--ink);
}

/* The pages */
.stage {
  height: 400px;
  border: 1px solid var(--line);
  border-radius: 14px;
  background: var(--canvas);
  box-shadow:
    0 1px 2px var(--shadow),
    0 18px 40px -28px rgb(7 32 76 / 0.28);
}

.loading {
  margin: 0;
  padding: 24px 0;
  text-align: center;
  font-size: 13px;
  color: var(--muted);
}
```

## Print

`print()` prints the document, not your app: its pages, each on a sheet of its own size, through the
browser's print dialog. Like `download()`, it waits for changes still on their way first.

```ts
await documents.print();
await documents.print(undefined, { pages: [page.ref], includeAnnotations: false });
```

`pages` takes page refs and prints them in that order; without it, every page prints. A page prints
as the PDF says it prints, which isn't always how it looks on screen: an annotation or a form field
whose print setting is off stays off the paper, and a layer meant for paper only shows up on it.
`includeAnnotations: false` leaves the annotations out; form fields always print.

When the [actions plugin](actions) is registered, the PDF's own print actions run around the
browser's dialog, as they would in Acrobat.

### How sharp it prints

On a desktop browser with its own PDF viewer on, a document that allows high-quality printing and
keeping a copy prints as a PDF: a copy of the pages as they print, which the browser's viewer prints
with sharp text and lines. Every print dialog can save what it prints as a file, so the copy takes
a right to keep it. Everywhere else the browser prints pictures of the pages:

| Permission                                                      | Prints                                                                               |
| :-------------------------------------------------------------- | :----------------------------------------------------------------------------------- |
| `doc.print.high` and `doc.download` or `doc.download.flattened` | The PDF copy; pictures at 300 DPI on phones, tablets and Chrome set to download PDFs |
| `doc.print.high`                                                | Pictures at 300 DPI                                                                  |
| `doc.print` only                                                | Pictures at 150 DPI, as a PDF asks when it allows only low-resolution printing       |

`getPrintQuality()` says which, for a note in your dialog: `{ kind: 'pdf' }`,
`{ kind: 'images', dpi: 300 }`, or `null` when the document can't be printed here. When the
browser turns the PDF down after all, the same pages print as pictures at 300 DPI, and the
prepared print's `quality` says so.

### Your own print dialog

Drawing the pages takes a moment, and costs work for every page, on the server too with a cloud
document. So a dialog of your own lets the user choose first, and prepares when they press Print,
with its progress:

```ts
printButton.onclick = async () => {
  const prepared = await documents.preparePrint(undefined, {
    pages,
    signal,
    onProgress: ({ pagesRendered, pageCount }) => setProgress(pagesRendered / pageCount),
  });
  if (prepared.needsTap) showReady(() => prepared.print());
  else await prepared.print();
};
```

Safari prints only from a tap or click (later, iOS refuses the print dialog and the Mac prints
blank pages), and the one on Print went to preparing. There `prepared.needsTap` is `true`: show that the printout is ready, and print from the next
tap. Other browsers print as soon as it is ready. `documents.print()` prepares and prints in one
call, so in Safari, prepare first.

`prepared.print()` prepares again first when the document changed since, and resolves when the
browser's dialog closes. Cancel a printout on its way with its `signal`, or `dispose()` the one
that was ready; closing the document frees it too. The viewer's Print dialog works this way.

## Warn before changes are lost

`useDocument()` tells you whether the document has changes that haven't been downloaded yet:

**`unsaved-warning.tsx`**

```tsx
import { useEffect } from 'react';
import { useDocument } from '@embedpdf/react/runtime';

export function UnsavedWarning() {
  const { hasUnsavedChanges } = useDocument();

  useEffect(() => {
    if (!hasUnsavedChanges) return;
    const warn = (event: BeforeUnloadEvent) => event.preventDefault();
    window.addEventListener('beforeunload', warn);
    return () => window.removeEventListener('beforeunload', warn);
  }, [hasUnsavedChanges]);

  return hasUnsavedChanges ? <span>Unsaved changes</span> : null;
}
```

It becomes `false` again after a `download()`. The document's other fields are listed on
[Opening documents](opening#state).

**`unsaved.tsx`**

```tsx
import { useEffect, useState } from 'react';
import {
  Viewer,
  DocumentGate,
  saveFile,
  useDocument,
  useDocuments,
  useDocumentsEvent,
} from '@embedpdf/react/runtime';
import type { OpenInput } from '@embedpdf/react/runtime';
import { Stage, stagePlugin } from '@embedpdf/react/stage';
import { RenderLayer, renderPlugin } from '@embedpdf/react/render';
import { metadataPlugin, useMetadata, useMetadataState } from '@embedpdf/react/metadata';
import { cloudEngine } from '@cloudpdf/engine';

import './unsaved.css';

const engine = cloudEngine();
const plugins = [stagePlugin(), renderPlugin(), metadataPlugin()];

const ebook: OpenInput = { kind: 'share', shareToken: 'shr_WGj1goAtlNN_fQ5OswPrbJQM' };

function TitleBar() {
  const documents = useDocuments();
  const metadata = useMetadata();
  const title = useMetadataState((state) => state.metadata?.title ?? '');
  const { hasUnsavedChanges } = useDocument();

  // A change on load: the document gets a new title, so it has something to lose.
  useEffect(() => {
    void metadata.update({ title: 'Quarterly report (draft)' });
  }, [metadata]);

  // While there's something to lose, the browser asks before the page closes.
  useEffect(() => {
    if (!hasUnsavedChanges) return;
    const warn = (event: BeforeUnloadEvent) => event.preventDefault();
    window.addEventListener('beforeunload', warn);
    return () => window.removeEventListener('beforeunload', warn);
  }, [hasUnsavedChanges]);

  return (
    <div className="toolbar">
      <input
        className="field"
        aria-label="Title"
        defaultValue={title}
        key={title}
        onBlur={(event) => metadata.update({ title: event.target.value })}
      />
      <span className="badge" data-unsaved={hasUnsavedChanges}>
        {hasUnsavedChanges ? 'Unsaved changes' : 'Downloaded'}
      </span>
      <button
        type="button"
        className="button"
        onClick={async () => saveFile(await documents.download(), 'report.pdf')}
      >
        Download
      </button>
    </div>
  );
}

function ChangeLog() {
  const [entries, setEntries] = useState<string[]>([]);
  useDocumentsEvent(
    (documents) => documents.onUnsavedChangesChanged,
    ({ hasUnsavedChanges }) =>
      setEntries((previous) => [
        hasUnsavedChanges
          ? 'It has changes that weren’t downloaded'
          : 'Downloaded: nothing to lose',
        ...previous,
      ]),
  );
  return (
    <ul className="log" aria-live="polite">
      {entries.slice(0, 3).map((entry, index) => (
        <li key={entries.length - index}>{entry}</li>
      ))}
    </ul>
  );
}

export default function App() {
  return (
    <Viewer
      engine={engine}
      plugins={plugins}
      initialDocuments={[{ source: ebook, name: 'report.pdf' }]}
    >
      <DocumentGate fallback={<p className="loading">Opening…</p>}>
        <TitleBar />
        <ChangeLog />
        <Stage className="stage">{() => <RenderLayer />}</Stage>
      </DocumentGate>
    </Viewer>
  );
}
```

**`unsaved.css`**

```css
/* Colors */
:root {
  --ink: #1a2748;
  --muted: #5a6b92;
  --line: #e6eaf2;
  --line-strong: #dce8fc;
  --surface: #ffffff;
  --tint: #f3f7fe;
  --canvas: #f7f9fc;
  --focus: #7db6ff;
  --unsaved: #b54708;
  --unsaved-tint: #fffaeb;
  --saved: #067647;
  --saved-tint: #ecfdf3;
  --shadow: rgb(7 32 76 / 0.04);
}

/* The title you change, whether it's downloaded, and the download */
.toolbar {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 8px;
  margin-bottom: 8px;
}

.field {
  appearance: none;
  flex: 1 1 180px;
  min-width: 0;
  font: inherit;
  font-size: 13px;
  color: var(--ink);
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: 9px;
  padding: 8px 12px;
}

.badge {
  font-size: 12px;
  font-weight: 600;
  border-radius: 999px;
  padding: 4px 10px;
  color: var(--saved);
  background: var(--saved-tint);
}

.badge[data-unsaved='true'] {
  color: var(--unsaved);
  background: var(--unsaved-tint);
}

.button {
  font: inherit;
  font-size: 13px;
  font-weight: 550;
  color: var(--ink);
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: 9px;
  padding: 8px 12px;
  cursor: pointer;
  box-shadow: 0 1px 2px var(--shadow);
}

.button:hover {
  background: var(--tint);
  border-color: var(--line-strong);
}

.field:focus-visible,
.button:focus-visible {
  outline: 2px solid var(--focus);
  outline-offset: 2px;
}

/* What `onUnsavedChangesChanged` said, newest first */
.log {
  margin: 0 0 14px;
  padding: 0;
  list-style: none;
  font-size: 12px;
  color: var(--muted);
}

/* The pages */
.stage {
  height: 380px;
  border: 1px solid var(--line);
  border-radius: 14px;
  background: var(--canvas);
  box-shadow:
    0 1px 2px var(--shadow),
    0 18px 40px -28px rgb(7 32 76 / 0.28);
}

.loading {
  margin: 0;
  padding: 24px 0;
  text-align: center;
  font-size: 13px;
  color: var(--muted);
}
```

On CloudPDF, changes are stored on your server as people make them, so `hasUnsavedChanges` stays
`false`: there's nothing to lose. Download when you want the file itself.

## Methods

What `useDocuments()` gives you for downloading and printing. Opening and closing are on
[Opening documents](opening#methods), tabs and the active document on
[Several documents](tabs#methods).

| Method                                                         | What it does                                                                                                                                                                                               |
| :------------------------------------------------------------- | :--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `download(id?, { mode })`                                      | The PDF with every change, as bytes. Waits for changes still on their way first. `mode` is `'incremental'` (the default) or `'rewrite'`. [A quick save or a full rewrite](#a-quick-save-or-a-full-rewrite) |
| `downloadLayer(id?)`                                           | Only the changes, as a layer you store and open again on top of the original. Local engine only; rejects when the document wasn't opened with a layer. [Keep only the changes](#keep-only-the-changes)     |
| `print(id?, { pages, includeAnnotations, onProgress })`        | Prints the document's pages through the browser's print dialog. [Print](#print)                                                                                                                            |
| `preparePrint(id?, { pages, includeAnnotations, onProgress })` | Draws the pages for printing, ready for the browser's dialog; resolves `{ quality, pageCount, needsTap, print(), dispose() }`. [Your own print dialog](#your-own-print-dialog)                             |
| `getPrintQuality(id?)`                                         | How it prints here: `{ kind: 'images', dpi }`, or `null` when it can't. [How sharp it prints](#how-sharp-it-prints)                                                                                        |
| `canDownload(id?)`, `canPrint(id?)`                            | Whether downloading and printing are allowed. [Permissions](#permissions)                                                                                                                                  |

Without an `id`, each uses the document in scope: the one a
[`<DocumentScope>`](tabs#talk-to-a-specific-document) around your component names, or else the
active one. Every method that returns a promise also takes a `signal` in its options, to cancel it.

## Events

**`title-dot.tsx`**

```tsx
import { useDocumentsEvent } from '@embedpdf/react/runtime';

export function TitleDot() {
  useDocumentsEvent(
    (documents) => documents.onUnsavedChangesChanged,
    ({ hasUnsavedChanges }) => {
      document.title = hasUnsavedChanges ? '● Contract' : 'Contract';
    },
  );

  return null;
}
```

| Event                     | Fires when                                                   | Carries                           |
| :------------------------ | :----------------------------------------------------------- | :-------------------------------- |
| `onUnsavedChangesChanged` | The document gets its first unsaved change, or is downloaded | `documentId`, `hasUnsavedChanges` |

## Permissions

| Permission       | What it allows                     | Check                         |
| :--------------- | :--------------------------------- | :---------------------------- |
| `doc.download`   | `download()` and `downloadLayer()` | `documents.canDownload()`     |
| `doc.print`      | `print()` and `preparePrint()`     | `documents.canPrint()`        |
| `doc.print.high` | Printing at 300 DPI instead of 150 | `documents.getPrintQuality()` |

Hide the buttons when the check says no. The engine refuses the call anyway.

## Good to know

### Signed documents

An incremental download keeps existing signatures valid, as long as the changes are ones the
signature allows. See [Signatures](../forms/signatures).
