ScreenshotNeo

BlogHow-to

How to export a React page to PDF after data loads

Wait for API data to render before exporting a React page to PDF. Compare browser printing, direct PDF generation, and server rendering.

By the ScreenshotNeo team4 October 20268 min read

Wait for the data request to finish and for React to render the resulting content before starting the PDF export. For browser printing, keep the export action unavailable while data is loading, then print a ref to the finished content. If you must prepare or change state immediately before printing, use react-to-print‘s Promise-based onBeforePrint hook and resolve it after React has committed the update.

Choose the output route first: browser printing gives the user the browser’s print dialog and save-to-PDF option; a separate PDF library or server-side HTML-to-PDF workflow is appropriate when your app must produce a file directly. react-to-print prints a component, but does not itself save a PDF without the print preview. See the react-to-print README.

1. Choose an export route

Route Use it when Readiness responsibility
Browser print The user can review print preview and choose Save as PDF. Wait until data is loaded and rendered; print the intended component.
Client-side PDF library The application needs to create a downloadable file through its own UI. Pass content that reflects the completed data state to the PDF workflow.
Server-rendered document and PDF pipeline The document should be rendered from a server-side React tree or generated outside the interactive page. Choose a rendering API that matches how the document obtains data and handles Suspense.

There is no universally best route for fidelity or speed: the result depends on your document, browser, styles, and generation pipeline. Validate the route with the actual content and target environment.

2. Browser print with react-to-print

Install the version of react-to-print that matches your application and check its current README for version-specific setup. The example below keeps the print button disabled until the request succeeds. It attaches a ref to the document content, so the user can open print preview and choose Save as PDF.

import { useRef, useState } from "react";
import { useReactToPrint } from "react-to-print";

function ReportPage() {
  const contentRef = useRef(null);
  const [report, setReport] = useState(null);
  const [status, setStatus] = useState("loading");
  const [error, setError] = useState("");

  async function loadReport() {
    setStatus("loading");
    setError("");
    try {
      const response = await fetch("/api/report");
      if (!response.ok) {
        throw new Error(`Report request failed: ${response.status}`);
      }
      const data = await response.json();
      setReport(data);
      setStatus("ready");
    } catch (err) {
      setError(err instanceof Error ? err.message : "Could not load report");
      setStatus("error");
    }
  }

  const printReport = useReactToPrint({
    contentRef,
    documentTitle: "report",
  });

  return (
    <main>
      <button type="button" onClick={loadReport}>Load report</button>
      {status === "loading" && <p>Loading report…</p>}
      {status === "error" && <p role="alert">{error}</p>}
      <button
        type="button"
        onClick={printReport}
        disabled={status !== "ready" || !report}
      >
        Print or save as PDF
      </button>

      {report && (
        <article ref={contentRef} className="report-print-area">
          <h1>{report.title}</h1>
          <p>{report.summary}</p>
          <table>
            <thead>
              <tr><th>Item</th><th>Value</th></tr>
            </thead>
            <tbody>
              {report.rows.map((row) => (
                <tr key={row.id}>
                  <td>{row.label}</td>
                  <td>{row.value}</td>
                </tr>
              ))}
            </tbody>
          </table>
        </article>
      )}
    </main>
  );
}

In JSX source, the markup uses normal angle brackets; they are escaped in this article’s HTML representation. Adapt the endpoint and fields to your application. The important sequence is: await the request, store its result, render the data, and only then enable printing. A state setter schedules an update; calling it alone does not prove the DOM already reflects the new state.

Coordinate last-minute preparation with onBeforePrint

If you need to switch to a print-specific view or prepare state immediately before printing, return a Promise from onBeforePrint. Resolve it after React has committed the update. The library’s README demonstrates this coordination pattern; follow the API for the installed version.

import { useRef, useState } from "react";
import { useReactToPrint } from "react-to-print";

function PrintableReport({ report }) {
  const contentRef = useRef(null);
  const [printReady, setPrintReady] = useState(false);
  const resolvePreparation = useRef(null);

  const print = useReactToPrint({
    contentRef,
    documentTitle: "report",
    onBeforePrint: () => {
      setPrintReady(true);
      return new Promise((resolve) => {
        resolvePreparation.current = resolve;
      });
    },
    onAfterPrint: () => setPrintReady(false),
  });

  // Resolve only after React has committed the print-specific state.
  // The effect runs after the corresponding render has been committed.
  useEffect(() => {
    if (printReady && resolvePreparation.current) {
      resolvePreparation.current();
      resolvePreparation.current = null;
    }
  }, [printReady]);

  return (
    <>
      <button type="button" onClick={print}>Print report</button>
      <article ref={contentRef} className={printReady ? "print-ready" : ""}>
        <h1>{report.title}</h1>
        <p>{report.summary}</p>
      </article>
    </>
  );
}

Include useEffect in the import in this second example: import { useEffect, useRef, useState } from "react";. If preparation can fail, make sure your application handles that failure and does not leave the Promise pending indefinitely. Avoid using a timer as proof that React has rendered; a delay only waits for time, not for a particular committed UI state.

Keep print styles attached to the printed component. Styles that rely on ancestors outside the print DOM may not apply to the content that is printed. Use print media rules to hide interactive controls and shape the page:

@media print {
  .screen-only,
  button,
  nav {
    display: none !important;
  }

  .report-print-area {
    color: #111;
    background: #fff;
    width: 100%;
  }

  .report-print-area table {
    width: 100%;
    border-collapse: collapse;
  }

  .report-print-area tr {
    break-inside: avoid;
  }

  @page {
    size: A4 portrait;
    margin: 16mm;
  }
}

Browser print dialogs may let users change paper size, margins, scale, headers, and background graphics. Browser behavior and support for print hints can differ, so preview output in the browsers you support. Inspect long tables, page breaks, images, and fonts with realistic data.

3. Direct PDF generation and server rendering

If your product needs a direct download instead of print preview, use a PDF-generation library or a separate document pipeline. The react-to-print README describes using a custom print handler to pass its gathered content to another PDF library. That means the PDF creation and download behavior belong to that library and your application; react-to-print does not provide a direct PDF download by itself.

Keep the same readiness rule: only hand the generator content after the data is available and the document representation is complete. Check the chosen library’s current documentation for its input format, font and image handling, pagination controls, browser requirements, and download or server APIs. The research sources do not establish a best PDF library or a general performance ranking.

For server-side React output, choose APIs based on Suspense behavior. React’s prerender waits for suspended data before resolving and is intended for static server-side generation. By comparison, renderToStaticMarkup produces non-interactive HTML and emits a Suspense fallback immediately if a component suspends. Do not swap these APIs without checking whether the document depends on pending Suspense content.

4. Troubleshoot common export failures

Symptom Likely cause Fix
PDF is missing data or shows a loading state Export started before the request completed or before the resulting render. Disable export until the request succeeds and the data-backed view is rendered. For print-time state changes, await the Promise-based preparation step.
Calling setState does not update the printed content State updates are scheduled; the DOM is not guaranteed to be updated immediately. Coordinate with a committed render, such as an effect that resolves the onBeforePrint Promise.
The print button does nothing or the wrong region prints The ref is missing, points at the wrong node, or is not configured as expected by the installed library version. Check the ref target and current react-to-print API. Attach the ref directly to the intended printable component.
Styles disappear in print preview A selector depends on an ancestor that is absent from the print DOM, or the rule is screen-only. Target nodes inside the printed content and define print-specific styles with @media print.
Direct PDF download never appears The browser-print flow opens a print preview; it is not a file-generation API. Use a PDF library or another PDF workflow and connect it to a custom handler.
Server output contains a Suspense fallback renderToStaticMarkup encountered a suspended component. Use a rendering approach that waits for suspended data, such as React’s documented prerender, when it fits the server workflow.
Pages split a row, image, or section awkwardly Print pagination and layout differ from screen layout and may vary by browser. Adjust print CSS, test page-break behavior with representative content, and verify in the target browser or PDF pipeline.
Export is stuck after preparation The Promise returned by onBeforePrint was never resolved, often because the expected state transition did not occur. Ensure every success and error path settles preparation, and avoid waiting on a state condition that cannot become true.

5. Performance, reliability, and cost

  • Performance: this workflow has no single documented speed profile. Large reports, images, fonts, and the selected PDF pipeline affect work and output. Measure with representative documents if export time matters.
  • Reliability: represent loading, success, and failure as explicit states. Keep export unavailable until success, report request errors, and avoid relying on arbitrary delays. Verify the rendered document before handing it to an automated generator.
  • Output checks: inspect the longest tables, image loading, page breaks, fonts, orientation, and margins in the actual browser or generation pipeline. Identical results across browsers are not guaranteed by the cited documentation.
  • Cost: browser printing uses the browser print flow; the cited sources specify no universal monetary cost. A client or server PDF library may have its own licensing, infrastructure, or usage costs, which must be checked in that library’s terms and pricing.

6. Or skip the browser setup

If your goal is a PDF of a public URL after its page has loaded, ScreenshotNeo can return a PDF from one GET request. It is a website screenshot API and MCP server from ScreenshotNeo. This is URL capture, not a replacement for exporting private React state that only exists inside your authenticated application. See the API documentation for request options.

curl -G "https://api.screenshotneo.com/v1/shot" \
  -d access_key=YOUR_API_KEY \
  --data-urlencode url=https://stripe.com \
  -d format=pdf \
  -o page.pdf

The API also accepts the common screenshot parameters used by other screenshot APIs. For a React page that requires authentication or client-only data, use your own application export flow or configure the request’s supported headers, cookies, and wait conditions as appropriate. A public URL alone cannot supply data the service cannot access.

  • Cookie and consent banners are accepted and removed before the shot, along with known newsletter popups and chat widgets; each cleanup step can be turned off.
  • Bot checks, blank pages, timeouts, failed loads, and cache hits are not billed. Responses include X-Page-Verdict and X-Billed headers.
  • An MCP server lets AI agents using Claude, Cursor, or any MCP client use take_screenshot, get_page_info, and capture_pdf.
  • The free plan includes 1,000 shots each month with no card. Paid plans start at $5 for 3,000 shots; every feature is on every plan.

Sign up for ScreenshotNeo and get 1,000 screenshots a month free, with no card.

7. Frequently asked questions

Can react-to-print download a PDF without opening print preview?

No. Its documented role is printing a component; use another PDF-generation workflow for a direct file download.

Will waiting for the fetch Promise always mean the printed DOM is ready?

It establishes that the request completed, but state updates are asynchronous. Keep export behind the rendered ready state, or coordinate a print-specific update with the Promise hook.

Does React prerender create a PDF?

No. It produces static server-rendered output after suspended data resolves. A separate step must turn that output into a PDF.

Can ScreenshotNeo export data visible only after I sign in?

It can use supported custom headers, cookies, and wait conditions, but access depends on your page’s authentication and data setup. Do not send credentials you are not authorized to use.