There are two answers here, and picking the wrong one costs you a rewrite. The deciding question is whether the PDF should look like the page you already built.
If you want your existing component, render it to HTML first
React's job ends at a string:
import { renderToStaticMarkup } from 'react-dom/server'
function Invoice({ number, total }) {
return (
<html>
<head>
<style>{`@page { size: A4; margin: 20mm }
body { font-family: Helvetica }
.total { font-weight: 700 }`}</style>
</head>
<body>
<h1>Invoice {number}</h1>
<p className="total">Total: ${total}</p>
</body>
</html>
)
}
const html = '<!DOCTYPE html>' + renderToStaticMarkup(
<Invoice number="1042" total="4,820.00" />
)renderToStaticMarkup returns a plain HTML string with no hydration attributes. It does not emit a doctype, so prepend one yourself. From there the React part is over: that string is what you hand to page.setContent(html) followed by page.pdf() in Puppeteer, or send as the body of a request to a hosted HTML to PDF endpoint.
Two things that will bite you
Effects do not run. React's docs are explicit that effects only run on the client and do not run during server rendering. A component that loads its own data in useEffect renders with whatever its initial state was, which is usually an empty table or a spinner. Resolve the data first and pass it in as props.
The markup carries class names, not styles. renderToStaticMarkup emits class="total" and nothing else. Whatever renders the PDF needs the actual CSS, either inlined in a style tag as above or at a URL it can fetch. This is the most common reason a generated PDF comes out looking like unstyled 1996 HTML.
If you would rather not run a browser at all
@react-pdf/renderer draws the PDF directly, with no DOM and no Chromium to install:
import {
Document, Page, Text, View, StyleSheet, renderToBuffer,
} from '@react-pdf/renderer'
const styles = StyleSheet.create({
page: { padding: 40, fontSize: 12 },
total: { marginTop: 12, fontWeight: 'bold' },
})
function Invoice({ number, total }) {
return (
<Document>
<Page size="A4" style={styles.page}>
<View>
<Text>Invoice {number}</Text>
<Text style={styles.total}>Total: ${total}</Text>
</View>
</Page>
</Document>
)
}
const buffer = await renderToBuffer(
<Invoice number="1042" total="4,820.00" />
)renderToBuffer returns a Node Buffer you can write to disk or stream back in a response. The trade-off is that you cannot reuse existing markup. You rewrite the component against Document, Page, View and Text, and style it from an explicit list of supported CSS properties that is flexbox based: display accepts only flex and none, and CSS grid is not on the list.
So: if the PDF should be a faithful print of a page you already built, go the HTML string route. If it is its own artifact with its own layout, react-pdf is far less machinery. And because the first route hands off a plain HTML string, any HTML to PDF endpoint will take it, including Anvil's PDF generation API, which accepts your HTML and CSS as JSON and returns the PDF.
Back to All Questions