React PDF — Generate PDF Documents from React Components
Advertisement
Introduction
Why This Matters
PDF generation is a common requirement: invoices, reports, certificates, contracts. Doing it with raw PDF libraries is painful. React PDF lets you define documents with familiar component syntax and renders them to PDF on both the server and client, integrating naturally with Next.js API routes.
Installation
npm install @react-pdf/rendererMark usage as client-only or use server-side rendering via API routes (recommended for large documents).
Invoice PDF Component
// components/invoice-pdf.tsx
import {
Document,
Page,
Text,
View,
StyleSheet,
Font
} from '@react-pdf/renderer'
Font.register({
family: 'Inter',
fonts: [
{ src: 'https://fonts.gstatic.com/s/inter/v12/UcCO3FwrK3iLTeHuS_fvQtMwCp50KnMw2boKoduKmMEVuLyfAZ9hiJ-Ek-_EeA.woff', fontWeight: 400 },
{ src: 'https://fonts.gstatic.com/s/inter/v12/UcCO3FwrK3iLTeHuS_fvQtMwCp50KnMw2boKoduKmMEVuI6fAZ9hiJ-Ek-_EeA.woff', fontWeight: 700 }
]
})
const styles = StyleSheet.create({
page: { fontFamily: 'Inter', fontSize: 11, color: '#1a202c', padding: 48 },
header: { flexDirection: 'row', justifyContent: 'space-between', marginBottom: 32 },
companyName: { fontSize: 22, fontWeight: 700, color: '#1d4ed8' },
companyDetails: { fontSize: 9, color: '#718096', marginTop: 4, lineHeight: 1.5 },
invoiceTitle: { fontSize: 28, fontWeight: 700, textAlign: 'right', color: '#1a202c' },
invoiceMeta: { fontSize: 10, color: '#718096', textAlign: 'right', marginTop: 4 },
billTo: { marginBottom: 24 },
label: { fontSize: 9, color: '#718096', textTransform: 'uppercase', letterSpacing: 1, marginBottom: 4 },
clientName: { fontSize: 13, fontWeight: 700 },
clientDetail: { fontSize: 10, color: '#4a5568', lineHeight: 1.5 },
tableHeader: { flexDirection: 'row', backgroundColor: '#f7fafc', padding: '8 12', borderRadius: 4, marginBottom: 4 },
tableHeaderCell: { fontSize: 9, fontWeight: 700, textTransform: 'uppercase', color: '#718096' },
tableRow: { flexDirection: 'row', padding: '10 12', borderBottom: '1 solid #e2e8f0' },
cell: { fontSize: 10, color: '#1a202c' },
totals: { marginTop: 24, alignItems: 'flex-end' },
totalRow: { flexDirection: 'row', justifyContent: 'space-between', width: 200, marginBottom: 6 },
totalLabel: { fontSize: 10, color: '#718096' },
totalValue: { fontSize: 10, fontWeight: 700, color: '#1a202c' },
grandTotal: { borderTop: '2 solid #1d4ed8', paddingTop: 8, marginTop: 4 },
grandTotalLabel: { fontSize: 12, fontWeight: 700, color: '#1d4ed8' },
grandTotalValue: { fontSize: 14, fontWeight: 700, color: '#1d4ed8' }
})
type LineItem = { description: string; quantity: number; unitPrice: number }
type InvoiceData = {
invoiceNumber: string
issueDate: string
dueDate: string
client: { name: string; email: string; address: string }
items: LineItem[]
taxRate?: number
}
export function InvoicePDF({ data }: { data: InvoiceData }) {
const subtotal = data.items.reduce((sum, item) => sum + item.quantity * item.unitPrice, 0)
const tax = subtotal * (data.taxRate ?? 0)
const total = subtotal + tax
return (
<Document title={`Invoice ${data.invoiceNumber}`}>
<Page size="A4" style={styles.page}>
{/* Header */}
<View style={styles.header}>
<View>
<Text style={styles.companyName}>YourCompany</Text>
<Text style={styles.companyDetails}>123 Business Ave{'\n'}San Francisco, CA 94105{'\n'}billing@yourcompany.com</Text>
</View>
<View>
<Text style={styles.invoiceTitle}>INVOICE</Text>
<Text style={styles.invoiceMeta}>#{data.invoiceNumber}</Text>
<Text style={styles.invoiceMeta}>Issued: {data.issueDate}</Text>
<Text style={styles.invoiceMeta}>Due: {data.dueDate}</Text>
</View>
</View>
{/* Bill To */}
<View style={styles.billTo}>
<Text style={styles.label}>Bill To</Text>
<Text style={styles.clientName}>{data.client.name}</Text>
<Text style={styles.clientDetail}>{data.client.email}</Text>
<Text style={styles.clientDetail}>{data.client.address}</Text>
</View>
{/* Table Header */}
<View style={styles.tableHeader}>
<Text style={[styles.tableHeaderCell, { flex: 3 }]}>Description</Text>
<Text style={[styles.tableHeaderCell, { flex: 1, textAlign: 'center' }]}>Qty</Text>
<Text style={[styles.tableHeaderCell, { flex: 1.5, textAlign: 'right' }]}>Unit Price</Text>
<Text style={[styles.tableHeaderCell, { flex: 1.5, textAlign: 'right' }]}>Amount</Text>
</View>
{/* Line Items */}
{data.items.map((item, i) => (
<View key={i} style={styles.tableRow}>
<Text style={[styles.cell, { flex: 3 }]}>{item.description}</Text>
<Text style={[styles.cell, { flex: 1, textAlign: 'center' }]}>{item.quantity}</Text>
<Text style={[styles.cell, { flex: 1.5, textAlign: 'right' }]}>${item.unitPrice.toFixed(2)}</Text>
<Text style={[styles.cell, { flex: 1.5, textAlign: 'right', fontWeight: 700 }]}>${(item.quantity * item.unitPrice).toFixed(2)}</Text>
</View>
))}
{/* Totals */}
<View style={styles.totals}>
<View style={styles.totalRow}>
<Text style={styles.totalLabel}>Subtotal</Text>
<Text style={styles.totalValue}>${subtotal.toFixed(2)}</Text>
</View>
{data.taxRate && (
<View style={styles.totalRow}>
<Text style={styles.totalLabel}>Tax ({(data.taxRate * 100).toFixed(0)}%)</Text>
<Text style={styles.totalValue}>${tax.toFixed(2)}</Text>
</View>
)}
<View style={[styles.totalRow, styles.grandTotal]}>
<Text style={styles.grandTotalLabel}>Total Due</Text>
<Text style={styles.grandTotalValue}>${total.toFixed(2)}</Text>
</View>
</View>
</Page>
</Document>
)
}Server-Side Generation API Route
// app/api/invoices/[id]/pdf/route.ts
import { NextRequest, NextResponse } from 'next/server'
import { renderToBuffer } from '@react-pdf/renderer'
import { InvoicePDF } from '@/components/invoice-pdf'
export async function GET(
_request: NextRequest,
{ params }: { params: { id: string } }
) {
const invoice = await db.invoices.findUnique({
where: { id: params.id },
include: { client: true, items: true }
})
if (!invoice) {
return NextResponse.json({ error: 'Invoice not found' }, { status: 404 })
}
const buffer = await renderToBuffer(
<InvoicePDF data={{
invoiceNumber: invoice.number,
issueDate: invoice.createdAt.toLocaleDateString(),
dueDate: invoice.dueDate.toLocaleDateString(),
client: { name: invoice.client.name, email: invoice.client.email, address: invoice.client.address },
items: invoice.items,
taxRate: invoice.taxRate
}} />
)
return new NextResponse(buffer, {
headers: {
'Content-Type': 'application/pdf',
'Content-Disposition': `attachment; filename="invoice-${invoice.number}.pdf"`
}
})
}Download Button Component
'use client'
export function DownloadInvoiceButton({ invoiceId }: { invoiceId: string }) {
async function handleDownload() {
const res = await fetch(`/api/invoices/${invoiceId}/pdf`)
const blob = await res.blob()
const url = URL.createObjectURL(blob)
const a = document.createElement('a')
a.href = url
a.download = `invoice-${invoiceId}.pdf`
a.click()
URL.revokeObjectURL(url)
}
return (
<button onClick={handleDownload} className="flex items-center gap-2 bg-blue-600 text-white px-4 py-2 rounded-lg hover:bg-blue-700">
Download PDF
</button>
)
}Common Mistakes
- Using
classNameor Tailwind — React PDF usesStyleSheet.create()with its own CSS-like properties - Importing fonts from Google Fonts URLs that block in production — host fonts locally or use a CDN that allows cross-origin
- Using flexbox without specifying
flexDirection: 'row'— default iscolumn(same as CSS default) - Rendering large PDFs client-side — use the API route approach for documents with many pages
Best Practices
- Always generate PDFs server-side for sensitive data — do not pass invoice amounts or client data to the browser
- Register custom fonts with
Font.register()before defining styles - Use
renderToBuffer()in API routes andrenderToStream()for streaming large documents
Key Takeaways
- React PDF uses its own layout engine based on flexbox — not a browser renderer
- Use
StyleSheet.create()for all styles —className, Tailwind, and CSS files do not work renderToBuffer()in a Next.js API route is the correct approach for server-side PDF generation- Always set
Content-Disposition: attachmentin the response to trigger a download instead of browser rendering Font.register()must be called before using the font family in styles- The default flex direction is
column— addflexDirection: 'row'for horizontal layouts Pagesupportssize(A4,LETTER) andorientation(portrait,landscape)- For sensitive documents, always generate server-side — never pass protected data to client-side code
Advertisement