React PDF
"React-PDF (@react-pdf/renderer): PDF generation with React components, styled documents, dynamic content, tables, images, fonts"
React-PDF lets you build PDFs using the same component model you use for UI. Instead of constructing low-level PDF instructions, you declare documents as a tree of `<Document>`, `<Page>`, `<View>`, and `<Text>` components with a flexbox-based styling system. Choose React-PDF when your team already thinks in React, when documents are data-driven with conditional sections, or when you want a type-safe, component-based approach to PDF layout. It runs in Node without a browser, making it lighter than headless Chrome solutions.
## Key Points
- Register fonts once at module load, not inside components or render calls.
- Use `StyleSheet.create()` for styles rather than inline objects; it enables internal caching.
- Mark repeating headers and footers with the `fixed` prop so they appear on every page.
- Use the `render` prop on `<Text>` for dynamic page numbers; it receives `{ pageNumber, totalPages }`.
- Prefer `renderToStream` over `renderToBuffer` for large documents to avoid holding the entire PDF in memory.
- Build reusable layout components (tables, headers, footers) and compose them; this is React's strength.
- Disable hyphenation with `Font.registerHyphenationCallback((word) => [word])` to prevent unexpected word breaks.
- Keep component trees shallow; deeply nested flex layouts slow down the layout engine.
- **Using HTML/CSS and expecting it to work.** React-PDF has its own layout engine; standard HTML elements and CSS properties are not supported. Only use the provided primitives.
- **Registering fonts inside components.** Font registration triggers re-downloads and slows rendering. Do it once at startup.
- **Ignoring the `break` and `wrap` props.** Without explicit page break control, content splits at arbitrary points. Use `break` to force breaks and `wrap={false}` to keep a section together.
- **Large inline images without optimization.** Embedding multi-megabyte images inflates PDF size. Resize and compress images before passing them.skilldb get document-generation-services-skills/React PDFFull skill: 355 linesReact-PDF Document Generation
Core Philosophy
React-PDF lets you build PDFs using the same component model you use for UI. Instead of constructing low-level PDF instructions, you declare documents as a tree of <Document>, <Page>, <View>, and <Text> components with a flexbox-based styling system. Choose React-PDF when your team already thinks in React, when documents are data-driven with conditional sections, or when you want a type-safe, component-based approach to PDF layout. It runs in Node without a browser, making it lighter than headless Chrome solutions.
Setup
// package.json dependencies
// "@react-pdf/renderer": "^3.4.0"
import React from "react";
import {
Document,
Page,
Text,
View,
Image,
Font,
StyleSheet,
renderToBuffer,
renderToStream,
Link,
} from "@react-pdf/renderer";
// Register custom fonts before any rendering
Font.register({
family: "Inter",
fonts: [
{ src: "./fonts/Inter-Regular.ttf", fontWeight: 400 },
{ src: "./fonts/Inter-Bold.ttf", fontWeight: 700 },
{ src: "./fonts/Inter-Italic.ttf", fontStyle: "italic" },
],
});
// Disable hyphenation for cleaner text rendering
Font.registerHyphenationCallback((word) => [word]);
Key Techniques
Basic Document Structure
const styles = StyleSheet.create({
page: {
fontFamily: "Inter",
fontSize: 11,
padding: 40,
lineHeight: 1.5,
color: "#1a1a1a",
},
header: {
fontSize: 24,
fontWeight: 700,
marginBottom: 20,
color: "#0f172a",
},
section: {
marginBottom: 16,
},
row: {
flexDirection: "row",
alignItems: "center",
},
label: {
fontWeight: 700,
width: 120,
},
divider: {
borderBottomWidth: 1,
borderBottomColor: "#e2e8f0",
marginVertical: 12,
},
});
interface InvoiceData {
number: string;
date: string;
customer: { name: string; address: string };
items: Array<{ description: string; qty: number; price: number }>;
}
const InvoiceDocument: React.FC<{ data: InvoiceData }> = ({ data }) => (
<Document>
<Page size="A4" style={styles.page}>
<Text style={styles.header}>Invoice #{data.number}</Text>
<View style={styles.section}>
<View style={styles.row}>
<Text style={styles.label}>Date:</Text>
<Text>{data.date}</Text>
</View>
<View style={styles.row}>
<Text style={styles.label}>Customer:</Text>
<Text>{data.customer.name}</Text>
</View>
</View>
<View style={styles.divider} />
<ItemsTable items={data.items} />
<View style={styles.divider} />
<TotalSection items={data.items} />
</Page>
</Document>
);
Table Component Pattern
const tableStyles = StyleSheet.create({
table: {
width: "100%",
},
headerRow: {
flexDirection: "row",
backgroundColor: "#f1f5f9",
padding: 8,
fontWeight: 700,
fontSize: 10,
textTransform: "uppercase",
letterSpacing: 0.5,
},
row: {
flexDirection: "row",
padding: 8,
borderBottomWidth: 1,
borderBottomColor: "#f1f5f9",
},
colDescription: { flex: 3 },
colQty: { flex: 1, textAlign: "center" },
colPrice: { flex: 1, textAlign: "right" },
colTotal: { flex: 1, textAlign: "right" },
});
interface LineItem {
description: string;
qty: number;
price: number;
}
const ItemsTable: React.FC<{ items: LineItem[] }> = ({ items }) => (
<View style={tableStyles.table}>
<View style={tableStyles.headerRow}>
<Text style={tableStyles.colDescription}>Description</Text>
<Text style={tableStyles.colQty}>Qty</Text>
<Text style={tableStyles.colPrice}>Price</Text>
<Text style={tableStyles.colTotal}>Total</Text>
</View>
{items.map((item, i) => (
<View key={i} style={tableStyles.row}>
<Text style={tableStyles.colDescription}>{item.description}</Text>
<Text style={tableStyles.colQty}>{item.qty}</Text>
<Text style={tableStyles.colPrice}>${item.price.toFixed(2)}</Text>
<Text style={tableStyles.colTotal}>
${(item.qty * item.price).toFixed(2)}
</Text>
</View>
))}
</View>
);
const TotalSection: React.FC<{ items: LineItem[] }> = ({ items }) => {
const subtotal = items.reduce((sum, i) => sum + i.qty * i.price, 0);
const tax = subtotal * 0.1;
return (
<View style={{ alignItems: "flex-end", marginTop: 12 }}>
<View style={{ flexDirection: "row", width: 200, marginBottom: 4 }}>
<Text style={{ flex: 1 }}>Subtotal:</Text>
<Text style={{ textAlign: "right" }}>${subtotal.toFixed(2)}</Text>
</View>
<View style={{ flexDirection: "row", width: 200, marginBottom: 4 }}>
<Text style={{ flex: 1 }}>Tax (10%):</Text>
<Text style={{ textAlign: "right" }}>${tax.toFixed(2)}</Text>
</View>
<View style={{ flexDirection: "row", width: 200, fontWeight: 700 }}>
<Text style={{ flex: 1 }}>Total:</Text>
<Text style={{ textAlign: "right" }}>
${(subtotal + tax).toFixed(2)}
</Text>
</View>
</View>
);
};
Multi-Page Documents with Headers and Footers
const reportStyles = StyleSheet.create({
page: { padding: 50, paddingTop: 70, paddingBottom: 60, fontSize: 11 },
pageHeader: {
position: "absolute",
top: 20,
left: 50,
right: 50,
flexDirection: "row",
justifyContent: "space-between",
fontSize: 8,
color: "#94a3b8",
},
pageFooter: {
position: "absolute",
bottom: 20,
left: 50,
right: 50,
textAlign: "center",
fontSize: 8,
color: "#94a3b8",
},
});
const ReportPage: React.FC<{
title: string;
children: React.ReactNode;
}> = ({ title, children }) => (
<Page size="A4" style={reportStyles.page}>
<View style={reportStyles.pageHeader} fixed>
<Text>{title}</Text>
<Text>Confidential</Text>
</View>
{children}
<Text
style={reportStyles.pageFooter}
fixed
render={({ pageNumber, totalPages }) =>
`Page ${pageNumber} of ${totalPages}`
}
/>
</Page>
);
Images and Dynamic Content
const CertificateDocument: React.FC<{
recipientName: string;
courseName: string;
completionDate: string;
logoUrl: string;
signatureUrl: string;
}> = ({ recipientName, courseName, completionDate, logoUrl, signatureUrl }) => (
<Document>
<Page
size="A4"
orientation="landscape"
style={{ padding: 60, alignItems: "center", justifyContent: "center" }}
>
<Image src={logoUrl} style={{ width: 120, marginBottom: 30 }} />
<Text style={{ fontSize: 32, fontWeight: 700, marginBottom: 10 }}>
Certificate of Completion
</Text>
<Text style={{ fontSize: 14, color: "#64748b", marginBottom: 30 }}>
This is to certify that
</Text>
<Text style={{ fontSize: 22, fontWeight: 700, marginBottom: 30 }}>
{recipientName}
</Text>
<Text style={{ fontSize: 14, color: "#64748b", marginBottom: 6 }}>
has successfully completed
</Text>
<Text style={{ fontSize: 16, fontWeight: 700, marginBottom: 40 }}>
{courseName}
</Text>
<View style={{ flexDirection: "row", alignItems: "flex-end", gap: 60 }}>
<View style={{ alignItems: "center" }}>
<Image src={signatureUrl} style={{ width: 100, height: 40 }} />
<View style={{ borderTopWidth: 1, borderTopColor: "#1a1a1a", width: 140, marginTop: 4 }} />
<Text style={{ fontSize: 9, marginTop: 4 }}>Instructor</Text>
</View>
<View style={{ alignItems: "center" }}>
<Text style={{ fontSize: 12 }}>{completionDate}</Text>
<View style={{ borderTopWidth: 1, borderTopColor: "#1a1a1a", width: 140, marginTop: 4 }} />
<Text style={{ fontSize: 9, marginTop: 4 }}>Date</Text>
</View>
</View>
</Page>
</Document>
);
Rendering to Buffer or Stream
import { renderToBuffer, renderToStream } from "@react-pdf/renderer";
import { Writable } from "stream";
// Render to buffer for APIs that return the full file
async function generateInvoicePdf(data: InvoiceData): Promise<Buffer> {
return renderToBuffer(<InvoiceDocument data={data} />);
}
// Render to stream for large documents or direct HTTP responses
async function streamInvoicePdf(
data: InvoiceData,
writable: Writable
): Promise<void> {
const stream = await renderToStream(<InvoiceDocument data={data} />);
stream.pipe(writable);
return new Promise((resolve, reject) => {
stream.on("end", resolve);
stream.on("error", reject);
});
}
// Express route example
import { Request, Response } from "express";
async function handleInvoiceDownload(req: Request, res: Response) {
const data = await fetchInvoiceData(req.params.id);
res.setHeader("Content-Type", "application/pdf");
res.setHeader("Content-Disposition", `attachment; filename=invoice-${data.number}.pdf`);
await streamInvoicePdf(data, res);
}
Best Practices
- Register fonts once at module load, not inside components or render calls.
- Use
StyleSheet.create()for styles rather than inline objects; it enables internal caching. - Mark repeating headers and footers with the
fixedprop so they appear on every page. - Use the
renderprop on<Text>for dynamic page numbers; it receives{ pageNumber, totalPages }. - Prefer
renderToStreamoverrenderToBufferfor large documents to avoid holding the entire PDF in memory. - Build reusable layout components (tables, headers, footers) and compose them; this is React's strength.
- Disable hyphenation with
Font.registerHyphenationCallback((word) => [word])to prevent unexpected word breaks. - Keep component trees shallow; deeply nested flex layouts slow down the layout engine.
Anti-Patterns
- Using HTML/CSS and expecting it to work. React-PDF has its own layout engine; standard HTML elements and CSS properties are not supported. Only use the provided primitives.
- Registering fonts inside components. Font registration triggers re-downloads and slows rendering. Do it once at startup.
- Ignoring the
breakandwrapprops. Without explicit page break control, content splits at arbitrary points. Usebreakto force breaks andwrap={false}to keep a section together. - Large inline images without optimization. Embedding multi-megabyte images inflates PDF size. Resize and compress images before passing them.
- Styling with percentages for everything. The flexbox engine handles most layouts, but percentage-based sizing can produce unexpected results for nested elements. Use fixed dimensions or flex ratios.
- Rendering on every HTTP request without caching. PDF generation is CPU-intensive. Cache generated PDFs by content hash when the underlying data has not changed.
Install this skill directly: skilldb add document-generation-services-skills
Related Skills
Docraptor
"DocRaptor: HTML-to-PDF API, Prince XML engine, CSS print styles, headers/footers, page breaks, async documents"
Docusaurus
Docusaurus: React-based static site generator for documentation sites, versioned docs, MDX support, search integration, i18n
Jspdf
jsPDF: client-side and server-side PDF generation in JavaScript, tables, images, custom fonts, autotable plugin
Latex Node
LaTeX with Node.js: compile LaTeX documents programmatically, template-based PDF generation, mathematical typesetting, academic papers
Markdoc
Markdoc: Stripe's Markdown-based authoring framework for structured documentation, custom tags, validation, and renderers
PDF Lib
"pdf-lib: create and modify PDFs in JavaScript, form filling, page manipulation, embedding images/fonts, digital signatures"