Generate PDF Reports from React Components
You've built a beautiful dashboard in React. Now your users want to download it as a PDF. The typical approach involves installing Puppeteer, configuring a headless browser, and managing server resources. There's a simpler way.
This guide shows you how to render React components to HTML and convert them to PDFs using DocAPI.
The Approach
React components are just functions that return markup. The strategy:
- Render your React component to an HTML string
- Include the necessary CSS
- Send the HTML to DocAPI
- Get back a PDF
No headless browsers. No complex dependencies.
Basic Example
Let's start with a simple report component:
// ReportTemplate.jsx
export function ReportTemplate({ data }) {
return (
<div className="report">
<header>
<h1>Monthly Sales Report</h1>
<p>Generated on {new Date().toLocaleDateString()}</p>
</header>
<section className="summary">
<div className="stat">
<span className="label">Total Revenue</span>
<span className="value">${data.revenue.toLocaleString()}</span>
</div>
<div className="stat">
<span className="label">Orders</span>
<span className="value">{data.orders}</span>
</div>
<div className="stat">
<span className="label">Avg Order Value</span>
<span className="value">${(data.revenue / data.orders).toFixed(2)}</span>
</div>
</section>
<table>
<thead>
<tr>
<th>Product</th>
<th>Units Sold</th>
<th>Revenue</th>
</tr>
</thead>
<tbody>
{data.products.map((product, i) => (
<tr key={i}>
<td>{product.name}</td>
<td>{product.units}</td>
<td>${product.revenue.toLocaleString()}</td>
</tr>
))}
</tbody>
</table>
</div>
);
}Rendering to HTML
Use react-dom/server to render your component to a string:
import { renderToStaticMarkup } from "react-dom/server";
import { ReportTemplate } from "./ReportTemplate";
function generateReportHTML(data) {
const markup = renderToStaticMarkup(<ReportTemplate data={data} />);
return `
<!DOCTYPE html>
<html>
<head>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; }
.report { padding: 40px; }
header { margin-bottom: 40px; border-bottom: 2px solid #e5e7eb; padding-bottom: 20px; }
header h1 { font-size: 28px; color: #111827; }
header p { color: #6b7280; margin-top: 8px; }
.summary { display: flex; gap: 40px; margin-bottom: 40px; }
.stat { }
.stat .label { display: block; font-size: 14px; color: #6b7280; margin-bottom: 4px; }
.stat .value { font-size: 32px; font-weight: 600; color: #111827; }
table { width: 100%; border-collapse: collapse; }
th, td { padding: 12px 16px; text-align: left; border-bottom: 1px solid #e5e7eb; }
th { background: #f9fafb; font-weight: 600; font-size: 14px; color: #374151; }
td { color: #4b5563; }
</style>
</head>
<body>
${markup}
</body>
</html>
`;
}Generating the PDF
Now send the HTML to DocAPI:
async function generateReport(data) {
const html = generateReportHTML(data);
const response = await fetch("https://api.docapi.co/v1/pdf", {
method: "POST",
headers: {
"x-api-key": process.env.DOCAPI_KEY,
"Content-Type": "application/json",
},
body: JSON.stringify({
html,
options: {
format: "A4",
margin: { top: "20mm", bottom: "20mm", left: "20mm", right: "20mm" },
},
}),
});
return await response.arrayBuffer();
}Next.js API Route
Here's a complete API route for Next.js:
// app/api/reports/route.js
import { renderToStaticMarkup } from "react-dom/server";
import { ReportTemplate } from "@/components/ReportTemplate";
export async function POST(request) {
const data = await request.json();
// Render React component to HTML
const markup = renderToStaticMarkup(<ReportTemplate data={data} />);
const html = wrapWithStyles(markup);
// Generate PDF
const response = await fetch("https://api.docapi.co/v1/pdf", {
method: "POST",
headers: {
"x-api-key": process.env.DOCAPI_KEY,
"Content-Type": "application/json",
},
body: JSON.stringify({
html,
options: { format: "A4" },
}),
});
if (!response.ok) {
return Response.json({ error: "PDF generation failed" }, { status: 500 });
}
const pdf = await response.arrayBuffer();
return new Response(pdf, {
headers: {
"Content-Type": "application/pdf",
"Content-Disposition": 'attachment; filename="report.pdf"',
},
});
}Handling CSS-in-JS
If you're using styled-components, emotion, or similar libraries, you need to extract the styles:
Styled Components
import { renderToStaticMarkup } from "react-dom/server";
import { ServerStyleSheet } from "styled-components";
import { ReportTemplate } from "./ReportTemplate";
function generateReportHTML(data) {
const sheet = new ServerStyleSheet();
try {
const markup = renderToStaticMarkup(
sheet.collectStyles(<ReportTemplate data={data} />)
);
const styles = sheet.getStyleTags();
return `
<!DOCTYPE html>
<html>
<head>${styles}</head>
<body>${markup}</body>
</html>
`;
} finally {
sheet.seal();
}
}Emotion
import { renderToStaticMarkup } from "react-dom/server";
import createEmotionServer from "@emotion/server/create-instance";
import createCache from "@emotion/cache";
import { CacheProvider } from "@emotion/react";
import { ReportTemplate } from "./ReportTemplate";
function generateReportHTML(data) {
const cache = createCache({ key: "pdf" });
const { extractCriticalToChunks, constructStyleTagsFromChunks } =
createEmotionServer(cache);
const markup = renderToStaticMarkup(
<CacheProvider value={cache}>
<ReportTemplate data={data} />
</CacheProvider>
);
const chunks = extractCriticalToChunks(markup);
const styles = constructStyleTagsFromChunks(chunks);
return `
<!DOCTYPE html>
<html>
<head>${styles}</head>
<body>${markup}</body>
</html>
`;
}Including Charts
Charts are tricky because libraries like Recharts and Chart.js render to SVG or Canvas. SVG works well in PDFs:
// Use SVG-based charts (Recharts renders to SVG by default)
import { LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip } from "recharts";
export function ChartReport({ data }) {
return (
<div className="report">
<h1>Sales Trend</h1>
<LineChart width={600} height={300} data={data.monthly}>
<CartesianGrid strokeDasharray="3 3" />
<XAxis dataKey="month" />
<YAxis />
<Line type="monotone" dataKey="sales" stroke="#2563eb" strokeWidth={2} />
</LineChart>
</div>
);
}For Canvas-based charts, you'll need to convert them to images first.
Multi-Page Reports
React components naturally flow across pages. Control page breaks with CSS:
export function MultiPageReport({ data }) {
return (
<div className="report">
{/* Cover page */}
<section className="cover-page">
<h1>{data.title}</h1>
<p>Prepared for {data.client}</p>
</section>
{/* Summary - starts on new page */}
<section className="summary-page">
<h2>Executive Summary</h2>
<p>{data.summary}</p>
</section>
{/* Data tables */}
{data.sections.map((section, i) => (
<section key={i} className="data-section">
<h2>{section.title}</h2>
<DataTable rows={section.rows} />
</section>
))}
</div>
);
}.cover-page {
min-height: 100vh;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
}
.summary-page {
page-break-before: always;
}
.data-section {
page-break-before: always;
}
/* Keep table rows together */
tr {
page-break-inside: avoid;
}Reusing Your Existing Components
You don't need separate "PDF components." Reuse your existing React components with a print-specific stylesheet:
// Your existing component
export function Dashboard({ data }) {
return (
<div className="dashboard">
<StatsGrid stats={data.stats} />
<SalesChart data={data.sales} />
<TopProducts products={data.products} />
</div>
);
}
// PDF generation
function generatePDFHTML(data) {
const markup = renderToStaticMarkup(<Dashboard data={data} />);
return `
<!DOCTYPE html>
<html>
<head>
<link rel="stylesheet" href="https://your-cdn.com/dashboard.css">
<style>
/* PDF-specific overrides */
@media print {
.dashboard { padding: 20mm; }
.interactive-elements { display: none; }
}
</style>
</head>
<body>${markup}</body>
</html>
`;
}Full Example: Sales Dashboard PDF
// lib/generateDashboardPDF.js
import { renderToStaticMarkup } from "react-dom/server";
import { Dashboard } from "@/components/Dashboard";
const styles = `
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
color: #1f2937;
line-height: 1.5;
}
.dashboard { padding: 40px; }
.header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 40px;
padding-bottom: 20px;
border-bottom: 2px solid #e5e7eb;
}
.header h1 { font-size: 24px; }
.header .date { color: #6b7280; }
.stats-grid {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 20px;
margin-bottom: 40px;
}
.stat-card {
background: #f9fafb;
padding: 20px;
border-radius: 8px;
}
.stat-card .label { font-size: 14px; color: #6b7280; }
.stat-card .value { font-size: 28px; font-weight: 600; margin-top: 4px; }
.stat-card .change { font-size: 14px; margin-top: 4px; }
.stat-card .change.positive { color: #059669; }
.stat-card .change.negative { color: #dc2626; }
.section { margin-bottom: 40px; }
.section h2 { font-size: 18px; margin-bottom: 16px; }
table { width: 100%; border-collapse: collapse; }
th, td { padding: 12px; text-align: left; border-bottom: 1px solid #e5e7eb; }
th { background: #f9fafb; font-weight: 600; }
`;
export async function generateDashboardPDF(data) {
const markup = renderToStaticMarkup(<Dashboard data={data} />);
const html = `
<!DOCTYPE html>
<html>
<head>
<style>${styles}</style>
</head>
<body>${markup}</body>
</html>
`;
const response = await fetch("https://api.docapi.co/v1/pdf", {
method: "POST",
headers: {
"x-api-key": process.env.DOCAPI_KEY,
"Content-Type": "application/json",
},
body: JSON.stringify({
html,
options: {
format: "A4",
landscape: true,
margin: { top: "15mm", bottom: "15mm", left: "15mm", right: "15mm" },
displayHeaderFooter: true,
footerTemplate: `
<div style="font-size: 10px; color: #999; width: 100%; text-align: center;">
Page <span class="pageNumber"></span> of <span class="totalPages"></span>
</div>
`,
},
}),
});
if (!response.ok) {
throw new Error("PDF generation failed");
}
return await response.arrayBuffer();
}Tips for Better PDFs
1. Use System Fonts
Avoid custom fonts that might not render. Stick to system fonts:
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;2. Set Explicit Widths
Don't rely on viewport-based sizing:
.report {
width: 210mm; /* A4 width */
max-width: 210mm;
}3. Avoid Flexbox Gaps on Older Engines
Some PDF renderers don't support gap. Use margins instead:
.grid > * + * {
margin-left: 20px;
}4. Test with Print Preview
Your browser's print preview is a quick way to debug PDF layouts before generating.
Client-Side Download Button
Add a download button to your React app:
export function DownloadReportButton({ data }) {
const [loading, setLoading] = useState(false);
async function handleDownload() {
setLoading(true);
try {
const response = await fetch("/api/reports", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(data),
});
const blob = await response.blob();
const url = URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url;
a.download = "report.pdf";
a.click();
URL.revokeObjectURL(url);
} finally {
setLoading(false);
}
}
return (
<button onClick={handleDownload} disabled={loading}>
{loading ? "Generating..." : "Download PDF"}
</button>
);
}Next Steps
- Get your API key at docapi.co
- Start with a simple component and build up complexity
- Reuse your existing styles where possible
- Test print layouts in browser before generating
Need Help?
- Check out the API documentation
- Email us at [email protected]
- We're happy to review your React templates