Back to Blog

Generate PDFs in React: Invoice Templates & Server-Side Rendering

How to generate PDFs in React and Next.js using @react-pdf/renderer. Includes invoice templates, server-side rendering, streaming downloads, and S3 upload with working examples.

Viprasol Tech Team
13 min read
Updated 2026

React PDF Generation: Libraries, Templates, and Production Tips (2026)

Quick answer. You can generate PDFs directly from React components client-side instead of running a separate server-side rendering service. This reduces infrastructure complexity, improves response times, and keeps PDFs in sync with your designs. Use it for invoices, reports, and certificates, choosing the right library and avoiding common production pitfalls.

At Viprasol, we've spent years building web applications that need to generate dynamic PDFs on demand—invoices, reports, certificates, and more. What started as a simple requirement has evolved into a critical feature that our clients depend on. In this post, I'll share what we've learned about PDF generation in React, the libraries worth considering, and the pitfalls to avoid in production.

Why PDF Generation in React Matters

The shift toward client-side PDF generation has fundamentally changed how we build modern web applications. Instead of maintaining separate server-side PDF rendering services, we can now generate PDFs directly from our React components. This approach reduces infrastructure complexity, improves response times, and makes it easier to keep your design system in sync across both the web and PDF outputs.

For our clients—especially those in SaaS and e-commerce—PDF generation is often mission-critical. A financial reporting platform that can't generate accurate PDFs on time creates compliance issues. An invoicing system that produces garbled receipts damages customer trust. The stakes are real, which is why we take this feature seriously.

Choosing the Right PDF Library

The React PDF ecosystem is crowded, and not all libraries are created equal. Let me break down the most viable options we've tested:

React-PDF (Best for React-Native-Like Approach)

React-PDF is designed to work with PDF primitives directly. You write components as if you're building a layout system, which makes sense if you're coming from a React background. The library handles the rendering to PDF format.

Pros:

  • True React component model
  • Good performance for large documents
  • Excellent for programmatic PDFs
  • Strong community support

Cons:

  • Limited CSS support (flexbox works, but grid doesn't)
  • Need to learn a different component API
  • Not ideal if you want to reuse existing styled components

Puppeteer (Best for HTML-to-PDF)

Puppeteer is a headless browser automation library. It renders your HTML exactly as Chrome would, then exports to PDF. This is our go-to for production clients who need pixel-perfect accuracy.

Pros:

  • HTML and CSS rendering works exactly like in the browser
  • Can reuse existing React components directly
  • Handles complex layouts and media queries
  • Excellent for screenshots and visual testing

Cons:

  • Requires running a separate process (server-side typically)
  • Higher memory overhead
  • Longer generation times for high volumes
  • Need to manage headless browser lifecycle

jsPDF with html2canvas (The Simple Solution)

This combination converts your DOM to a canvas image, then wraps it in a PDF. It's quick to set up and works for simple documents.

Pros:

  • Minimal setup
  • Works in the browser
  • No server-side dependencies
  • Good for prototyping

Cons:

  • Image-based output (not searchable or selectable)
  • Quality issues with text rendering
  • Poor performance with large documents
  • Layout shifts when converting to PDF

pdfKit (Node.js Native)

For those who prefer working with PDFs at a lower level, pdfKit provides direct PDF creation in Node.js. This is useful when you're building a backend service.

Pros:

  • Fine-grained control over PDF structure
  • Great documentation
  • Pure JavaScript implementation
  • No external dependencies

Cons:

  • Steep learning curve
  • Not designed for React integration
  • Manual layout calculations
  • Overkill for most use cases

🌐 Looking for a Dev Team That Actually Delivers?

Most agencies sell you a project manager and assign juniors. Viprasol is different — senior engineers only, direct Slack access, and a 5.0★ Upwork record across 1000+ projects.

  • React, Next.js, Node.js, TypeScript — production-grade stack
  • Fixed-price contracts — no surprise invoices
  • Full source code ownership from day one
  • 90-day post-launch support included

Our Production Setup at Viprasol

For web development projects, we've standardized on a hybrid approach:

Client-side for interactive PDFs: We use React-PDF for generating PDFs that users can preview before downloading. This keeps the browser responsive and doesn't require backend resources.

Server-side for production PDFs: We use Puppeteer on a dedicated rendering service for final PDF generation. This ensures consistency and handles complex layouts that React-PDF can't support.

Flow diagram:

  1. User fills out form → React component updates state
  2. Preview uses React-PDF to show live update
  3. On submit → Send to backend
  4. Backend uses Puppeteer → Render React component to static HTML → Convert to PDF
  5. Return PDF to user or store in S3

This architecture keeps response times under 2 seconds even with thousands of concurrent users.

Building Your First React PDF Component

Let me show you a practical example using React-PDF. We're building an invoice generator:

Code:

import React from 'react';
import { Document, Page, Text, View, StyleSheet, Image } from '@react-pdf/renderer';

const styles = StyleSheet.create({
  page: {
    padding: 40,
    fontSize: 11,
    fontFamily: 'Helvetica'
  },
  header: {
    marginBottom: 20,
    borderBottom: '1px solid #000'
  },
  title: {
    fontSize: 24,
    fontWeight: 'bold',
    marginBottom: 10
  },
  table: {
    marginVertical: 20
  },
  tableRow: {
    display: 'flex',
    flexDirection: 'row',
    borderBottom: '1px solid #ccc',
    paddingVertical: 8
  },
  tableCell: {
    flex: 1
  }
});

export const InvoicePDF = ({ data }) => (
  <Document>
    <Page size="A4" style={styles.page}>
      <View style={styles.header}>
        <Text style={styles.title}>Invoice</Text>
        <Text>Invoice #: {data.invoiceNumber}</Text>
        <Text>Date: {new Date().toLocaleDateString()}</Text>
      </View>

      <View style={styles.table}>
        {data.items.map((item, idx) => (
          <View key={idx} style={styles.tableRow}>
            <Text style={styles.tableCell}>{item.description}</Text>
            <Text style={styles.tableCell}>${item.amount}</Text>
          </View>
        ))}
      </View>

      <Text>Total: ${data.total}</Text>
    </Page>
  </Document>
);

This component generates a properly formatted PDF. You can use it like:

Code:

import { PDFDownloadLink } from '@react-pdf/renderer';

<PDFDownloadLink document={<InvoicePDF data={invoiceData} />} fileName="invoice.pdf">
  Download Invoice
</PDFDownloadLink>
React - Generate PDFs in React: Invoice Templates & Server-Side Rendering

🚀 Senior Engineers. No Junior Handoffs. Ever.

You get the senior developer, not a project manager who relays your requirements to someone you never meet. Every Viprasol project has a senior lead from kickoff to launch.

  • MVPs in 4–8 weeks, full platforms in 3–5 months
  • Lighthouse 90+ performance scores standard
  • Works across US, UK, AU timezones
  • Free 30-min architecture review, no commitment

Essential Production Considerations

Performance Optimization

PDF generation can become a bottleneck. We've learned several optimization techniques:

  1. Lazy load data: Don't generate the entire PDF if only part of it is needed
  2. Cache templates: Keep static elements cached in memory
  3. Use worker threads: Offload PDF generation to background workers for large batches
  4. Implement request queuing: Don't allow unlimited concurrent PDF generations

Error Handling and Fallbacks

We always implement graceful degradation. If PDF generation fails, we provide an HTML version as a fallback. This prevents user-facing errors:

Code:

try {
  const pdf = await generatePDF(data);
  return downloadPDF(pdf);
} catch (error) {
  console.error('PDF generation failed:', error);
  return <FallbackHTMLView data={data} />;
}

Memory Management

Puppeteer can consume significant memory. We've implemented:

  1. Process pooling to reuse instances
  2. Page recycling after N renders
  3. Memory monitoring and auto-restart
  4. Configurable timeout limits

Compliance and Security

For SaaS development, PDF generation often involves sensitive data:

  • Always generate PDFs server-side for sensitive documents
  • Implement proper access controls
  • Use encryption for PDFs containing personal information
  • Audit all PDF generation activities
  • Store generated PDFs securely (consider ephemeral storage)

Advanced Techniques

Dynamic Styling Based on Data

Sometimes you need different layouts depending on content:

Code:

const getLayoutStyle = (pageCount) => {
  if (pageCount > 5) {
    return styles.compactLayout;
  }
  return styles.standardLayout;
};

Batch PDF Generation

For reports and exports:

Code:

async function generateBatch(items) {
  const pdfs = await Promise.all(
    items.map(item => generatePDF(item))
  );
  return mergePDFs(pdfs);
}

QR Codes and Barcodes

Include these for logistics:

Code:

import QRCode from 'qrcode.react';

<QRCode value={trackingUrl} size={100} />

Tools and Libraries Worth Exploring

LibraryBest ForComplexityPerformance
React-PDFComponent-based PDFsLowHigh
PuppeteerHTML→PDF conversionMediumMedium
jsPDFQuick prototypesVery LowLow
pdfKitLow-level controlHighHigh
PlaywrightModern alternative to PuppeteerMediumHigh

For more detailed comparisons, check out the official React-PDF documentation and Puppeteer API reference from the maintainers. These are essential resources for understanding library capabilities and limitations. You can also explore MDN's PDF specification guide for deeper PDF format knowledge.

Integrating with Cloud Solutions

For our cloud infrastructure projects, we've built PDF services that scale:

  • AWS Lambda: Puppeteer works with Lambda, though memory limits require optimization
  • Google Cloud Run: Better memory allocation makes this ideal for PDF generation
  • Azure Container Instances: Excellent for batch processing

We typically store the generated PDFs in S3 and return presigned URLs to clients rather than streaming directly.

Advanced Production Patterns

Watermarking and Signing

For document compliance, add watermarks:

Code:

<View style={styles.watermark}>
  <Text style={{ opacity: 0.3, transform: 'rotate(-45deg)' }}>
    CONFIDENTIAL
  </Text>
</View>

Digital signatures require more work but are essential for certain industries:

Code:

// Sign PDF using certificate
const { PDFDocument } = require('pdf-lib');
const fs = require('fs');

async function signPDF(pdfPath: string, certificatePath: string) {
  const pdfDoc = await PDFDocument.load(fs.readFileSync(pdfPath));
  const signature = createSignature(certificatePath);
  // Add signature to document
  return pdfDoc.save();
}

Templating with Dynamic Content

Create reusable templates that accept variable data:

Code:

interface TemplateContext {
  companyName: string;
  logoUrl: string;
  recipientName: string;
  items: Array<{ name: string; quantity: number; price: number }>;
  notes?: string;
}

export const InvoiceTemplate = ({ context }: { context: TemplateContext }) => (
  <Document>
    <Page style={styles.page}>
      {context.logoUrl && <Image src={context.logoUrl} style={styles.logo} />}
      <Text style={styles.companyName}>{context.companyName}</Text>
      <Text style={styles.recipientName}>Bill To: {context.recipientName}</Text>
      
      {/* Dynamic items rendering */}
      {context.items.map((item, idx) => (
        <View key={idx} style={styles.itemRow}>
          <Text style={styles.itemName}>{item.name}</Text>
          <Text style={styles.itemQty}>{item.quantity}</Text>
          <Text style={styles.itemPrice}>${item.price}</Text>
        </View>
      ))}
      
      {context.notes && <Text style={styles.notes}>{context.notes}</Text>}
    </Page>
  </Document>
);

Streaming Large PDFs

For very large documents, stream instead of generating in memory:

Code:

import { createWriteStream } from 'fs';

export async function streamLargePDF(
  filePath: string,
  onProgress?: (percent: number) => void
) {
  const doc = new PDFDocument();
  const stream = createWriteStream(filePath);

  doc.pipe(stream);

  // Generate content in chunks
  const totalChunks = 1000;
  for (let i = 0; i < totalChunks; i++) {
    doc.fontSize(12).text(**Page content ${i}**);
    if ((i + 1) % 100 === 0 && onProgress) {
      onProgress(((i + 1) / totalChunks) * 100);
    }
  }

  doc.end();
  
  return new Promise((resolve, reject) => {
    stream.on('finish', resolve);
    stream.on('error', reject);
  });
}

Common Pitfalls We've Fixed

Fonts Not Rendering: Always embed fonts in your PDF library. System fonts don't transfer to PDFs. Use a font service or include font files in your build.

Missing Images: Use absolute URLs or base64-encoded images. Relative paths won't work in PDFs. For production, serve images from a CDN and use full URLs.

Pagination Issues: Test with realistic data. A template that works with 1KB of text often breaks with 100KB. Use dynamic spacing calculations based on content length.

Timeout Problems: Set appropriate timeouts. Network requests inside PDF generation can cause hangs. Use reasonable defaults (30 seconds for Puppeteer).

Memory Leaks: Close browser instances properly. We've seen Puppeteer processes consuming 2GB after a few hundred generations. Implement proper cleanup in finally blocks.

Encoding Issues: Ensure UTF-8 encoding for special characters. Some libraries default to ISO-8859-1.

File Permissions: When storing PDFs, ensure proper file system permissions. On cloud infrastructure, use temporary storage for ephemeral PDFs.

Specific Use Cases and Solutions

Certificates and Credentials

When generating certificates, precision matters:

Code:

interface CertificateData {
  recipientName: string;
  achievementTitle: string;
  dateIssued: Date;
  certificateId: string;
  issuerSignature: string;
}

export const CertificatePDF = ({ data }: { data: CertificateData }) => (
  <Document>
    <Page size={[1000, 700]} style={styles.certificatePage}>
      <View style={styles.border}>
        <Text style={styles.title}>Certificate of Achievement</Text>
        <Text style={styles.body}>This certifies that</Text>
        <Text style={styles.recipientName}>{data.recipientName}</Text>
        <Text style={styles.body}>has successfully completed</Text>
        <Text style={styles.achievement}>{data.achievementTitle}</Text>
        <Text style={styles.date}>Date: {data.dateIssued.toLocaleDateString()}</Text>
        <Text style={styles.certificateId}>ID: {data.certificateId}</Text>
        <Image src={data.issuerSignature} style={styles.signature} />
      </View>
    </Page>
  </Document>
);

Reports with Charts and Graphs

Combine data visualization with PDF generation:

Code:

import { Chart as ChartJS } from 'chart.js';
import { png } from 'chart.js-to-image';

export async function generateReportPDF(reportData: ReportData) {
  // Generate chart as image
  const chartImage = await png({
    type: 'bar',
    data: reportData.chartData,
    options: { responsive: true }
  });

  return (
    <Document>
      <Page>
        <Text style={styles.title}>{reportData.title}</Text>
        <Image src={chartImage} style={styles.chart} />
        <Text style={styles.summary}>{reportData.summary}</Text>
      </Page>
    </Document>
  );
}

Multi-Language Support

Handle different languages and RTL text:

Code:

export const MultiLanguagePDF = ({ content, language }: Props) => {
  const isRTL = ['ar', 'he', 'fa'].includes(language);
  
  return (
    <Document>
      <Page style={{ ...styles.page, direction: isRTL ? 'rtl' : 'ltr' }}>
        <Text style={styles.title}>{content[language].title}</Text>
        <Text>{content[language].body}</Text>
      </Page>
    </Document>
  );
};

FAQ

Q: Can I use CSS Grid in React-PDF? A: No, React-PDF only supports flexbox. For complex layouts, consider Puppeteer.

Q: How do I handle multi-page PDFs? A: React-PDF automatically paginates when content exceeds page height. Puppeteer renders everything the browser would render.

Q: Should I generate PDFs client-side or server-side? A: Client-side for preview and simple documents. Server-side for final production PDFs and anything involving sensitive data.

Q: What's the maximum file size I can generate? A: This depends on your library choice. React-PDF handles hundreds of pages. Puppeteer typically works well up to 50-100 pages before hitting memory limits.

Q: Can I include videos or interactive elements? A: PDFs are static by nature. You can include links and buttons, but not embedded video. For interactive content, use HTML.

Q: How do I test PDF generation? A: Use libraries like pdf-parse to extract and verify content. For visual testing, compare rendered PDFs using image diffing tools.

Moving Forward

PDF generation in React has matured significantly. We're no longer constrained by outdated tools. The key is choosing the right library for your use case and planning your architecture early.

Start simple if you're new to this. Use jsPDF for learning. Move to React-PDF when you need better quality. Implement Puppeteer when you need production-grade reliability and don't mind the server-side complexity.

The teams we work with—both in web development and SaaS—benefit enormously from getting PDF generation right early. It saves countless hours of debugging later and ensures your users get reliable, professional-looking documents every time.

Fixing borderBottom in @react-pdf/renderer style props

A common stumbling block is getting a borderBottom in @react-pdf/renderer to style invoice rows correctly. Unlike browser CSS, the renderer parses border properties strictly, so the cleanest approach is the longhand triplet: set borderBottomWidth to a number, borderBottomColor to a hex value, and borderBottomStyle to solid. The shorthand string also works, but mixing it with other border keys often gets silently dropped. Apply these inside a StyleSheet.create object rather than inline for predictable rendering and easier reuse across table headers and totals. If a line vanishes, check that the parent View has a defined width and is not flex-shrinking. These are exactly the rendering quirks our senior engineers handle when we build production PDF pipelines end to end, with full ownership of layout, fonts, and server-side generation.

ReactTypeScriptPDFNext.jsAWSSaaS
Share this article:

About the Author

V

Viprasol Tech Team

Custom Software Development Specialists

The Viprasol Tech team specialises in algorithmic trading software, AI agent systems, and SaaS development. With 1000+ projects delivered across MT4/MT5 EAs, fintech platforms, and production AI systems, the team brings deep technical experience to every engagement.

MT4/MT5 EA DevelopmentAI Agent SystemsSaaS DevelopmentAlgorithmic Trading

Need a Modern Web Application?

From landing pages to complex SaaS platforms — we build it all with Next.js and React.

Free consultation • No commitment • Response within 24 hours

Viprasol · Web Development

Need a custom web application built?

We build React and Next.js web applications with Lighthouse ≥90 scores, mobile-first design, and full source code ownership. Senior engineers only — from architecture through deployment.