Skip to main content
Technology & EngineeringDocument Generation Services355 lines

React PDF

"React-PDF (@react-pdf/renderer): PDF generation with React components, styled documents, dynamic content, tables, images, fonts"

Quick Summary18 lines
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 lines
Paste into your CLAUDE.md or agent config

React-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 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.

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 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.
  • 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

Get CLI access →