Every React PDF library I've used has the same dirty secret: they make you throw away your components and rebuild everything from scratch.
I've spent the last year deep in this problem, and I want to share what I learned — including the approaches that wasted my time and the one that finally clicked.
The Problem Nobody Talks About
Let's say you have a beautiful invoice component. It uses Tailwind, maybe some MUI elements, custom fonts, the works. Your client says: "Can users download this as a PDF?"
Simple request. Absolute nightmare.
Here's what most libraries expect you to do:
@react-pdf/renderer — the most popular option with ~16k GitHub stars — requires you to rewrite your entire UI using its proprietary primitives:
// Your actual component (the one that looks great on screen)
<div className="p-8 bg-white rounded-lg shadow">
<h1 className="text-2xl font-bold text-gray-900">Invoice #1042</h1>
<table className="w-full mt-4">...</table>
</div>
// What @react-pdf/renderer needs (a completely separate version)
<Document>
<Page size="A4" style={styles.page}>
<View style={styles.container}>
<Text style={styles.heading}>Invoice #1042</Text>
{/* Rebuild your entire table with View/Text primitives */}
</View>
</Page>
</Document>
No <div>. No <table>. No <span>. No CSS classes. You're writing in a parallel universe that looks like React but isn't your actual app.
jsPDF — gives you an imperative canvas API. You're literally calling doc.text("Invoice", 10, 10) and positioning everything with x/y coordinates. It's like going from React back to jQuery... except worse.
pdfmake — uses a JSON-based document definition. Not JSX, not HTML, not CSS. A completely different mental model:
{
content: [
{ text: 'Invoice #1042', style: 'header' },
{
table: {
body: [['Item', 'Price'], ['Widget', '$10']]
}
}
]
}
react-to-print — takes a screenshot approach, which sounds great until your content exceeds one page and gets clipped. Text comes out as rasterized images, so zooming makes everything blurry.
Every option essentially says: "Your beautiful React components? Forget about them. Start over."
What I Actually Wanted
The ask was simple:
- Point at a React component — any component, with any styling framework
- Get a multi-page PDF that looks exactly like what's on screen
- Don't rewrite anything
That's it. Keep your Tailwind. Keep your MUI theme. Keep your styled-components. Whatever CSS the browser renders should be what ends up in the PDF.
The Approach That Worked
After trying everything above on a real project, I ended up building a library called @easypdf/react that takes a fundamentally different approach. Instead of reconstructing your layout in a proprietary format, it captures the actual rendered DOM.
The entire API is a single hook:
import { useEasyPdf } from "@easypdf/react";
function Invoice() {
const { pdfRef, downloadPDF, isLoading } = useEasyPdf({
pageSize: "A4",
footer: {
text: "Page {pageNumber} of {totalPages}",
align: "center",
},
});
return (
<div>
<button
onClick={() => downloadPDF(pdfRef, { filename: "invoice.pdf" })}
disabled={isLoading}
>
{isLoading ? "Generating..." : "Download PDF"}
</button>
{/* This is your ACTUAL component. No changes needed. */}
<div ref={pdfRef}>
<InvoiceComponent /> {/* Tailwind, MUI, whatever — it all works */}
</div>
</div>
);
}
Attach a ref. Call a function. Done.
The Hard Parts (That Took Months to Solve)
Building this wasn't just "wrap html2canvas in a hook." Here are the three problems that consumed most of the development time:
1. Smart Page Breaking
The naive approach to capture-based PDF generation is to render the entire element as one tall canvas, then slice it into page-sized chunks. This is how you end up with tables split mid-row, images cut in half, and headings orphaned at the bottom of pages.
EasyPDF analyzes the DOM structure and finds natural break points. Tables, images, list items, and any element with the .no-break class are kept intact across page boundaries.
2. SSR / Next.js Compatibility
This one was painful. Most browser-based PDF libraries break in Next.js because they reference window or document at import time. The App Router makes this worse because everything is a server component by default.
EasyPDF has zero browser globals at import time. All browser-dependent code is guarded internally. You just add "use client" to your component and it works:
"use client";
import { useEasyPdf } from "@easypdf/react";
// That's it. No dynamic imports, no typeof window checks.
3. Full CSS Fidelity
The PDF needs to look exactly like the screen. That means resolving computed styles from every possible source — Tailwind utility classes, CSS modules, styled-components' injected stylesheets, MUI's theme, inline styles, everything. If the browser renders it, the PDF should match.
Programmatic Mode (No DOM Required)
Sometimes you need to generate a PDF without displaying anything on screen — background invoice generation, email attachments, batch exports. EasyPDF handles this too:
const { createPDF } = useEasyPdf();
const handleBatchExport = () =>
createPDF(
<div style={{ padding: "32px", fontFamily: "Arial" }}>
<h1>Invoice #{data.id}</h1>
<p>Total: ${data.total}</p>
</div>,
{ filename: `invoice-${data.id}.pdf` }
);
The JSX is rendered off-screen, captured, and immediately cleaned up. It never appears in the visible DOM.
Honest Tradeoffs
No tool is perfect for every use case. Here's where EasyPDF fits and where it doesn't:
Use EasyPDF when:
- You have existing React components you want to export as PDFs
- You use Tailwind, MUI, shadcn, styled-components, or any CSS framework
- You need it to work in Next.js without SSR headaches
- You want multi-page PDFs with headers, footers, watermarks, and smart page breaks
Consider @react-pdf/renderer when:
- You need server-side generation without a browser (Node.js only)
- You need precise typographic control at the PDF primitive level
- You're building PDFs that don't have a corresponding web UI
Quick Comparison
| @react-pdf/renderer | jsPDF | pdfmake | react-to-print | @easypdf/react | |
|---|---|---|---|---|---|
| Use your existing components | ❌ | ❌ | ❌ | ⚠️ Single page | ✅ |
| CSS framework support | ❌ Custom styles only | ❌ | ❌ | ⚠️ Print CSS | ✅ Full |
| Multi-page | ✅ | ✅ Manual | ✅ | ❌ | ✅ Auto |
| Smart page breaks | ✅ | ❌ | ⚠️ Basic | ❌ | ✅ |
| Headers/Footers | ✅ | Manual | ✅ | ❌ | ✅ |
| SSR / Next.js safe | ⚠️ Needs config | ❌ | ❌ | ⚠️ | ✅ |
| Learning curve | High (new API) | High (imperative) | Medium (JSON) | Low | Low |
Try It
npm install @easypdf/react
- Repo Website [https://easypdf.vercel.app]
- GitHub: github.com/Alpovka/EasyPDF-React
- Docs: easypdf.vercel.app/docs
- License: MIT — free and open source
If you've been fighting with PDF generation in React, I'd genuinely love to hear about your experience. In my company we found crazy ways to this solution that has overengineering and I have heard carzy solutions to this from some other people as well. So, how did you solve this issue? What's your biggest pain point? I built this because I got tired of maintaining two versions of every layout, and I'm curious if that resonates with others.
Look, I'm not going to stand here and pretend this library was delivered by the software gods on a golden USB stick. It has bugs. I know it has bugs. You'll probably find bugs I don't even know about yet — congratulations, you're now a contributor.
Here's how you can help:
Found a bug? → Open an issue. Bonus points if you include a reproduction. Infinite points if you also include the fix.
Have an idea? → Start a discussion. I'm genuinely open to "what if it could also do X" conversations.
Want to contribute? → PRs are welcome. The codebase is TypeScript, the vibes are friendly, and the bar for "better than what we had before" is honestly not that high.
Top comments (0)