PDFPipe

Migrating from @react-pdf/renderer / A declared document structure

Migrating off @react-pdf/renderer, from components to real CSS

A React renderer with its own primitives and its own subset of styling, where the component structure survives and everything about how it is styled changes.

What ports and what does not

The component tree survives as a shape. Its elements do not: Document, Page, View and Text are the library's own primitives with no HTML meaning, and they become div, section and the ordinary elements. The StyleSheet.create object is not CSS, it is a subset with its own supported properties, so it converts rather than transfers. What ports completely is your data flow, your props and your composition, because those are React and React is what you keep. In practice the migration turns a component that returns library primitives into one that returns markup, which is the same component with different leaves.

What the call becomes

renderToBuffer, renderToStream or the ReactPDF render call becomes renderToStaticMarkup from React's own server renderer, followed by a POST to /v1/pdf with the resulting html. The Page component's size prop becomes options.format, its orientation prop becomes options.landscape, and the style padding you were putting on Page becomes options.margin or a page rule. The fixed prop, which repeated an element on every page, becomes options.header_html and options.footer_html or a CSS margin box.

Side by side

The old shape and the new one, with the parts that have no counterpart called out in comments rather than quietly omitted.

jsx
// Before
const Invoice = () => (
  <Document>
    <Page size="A4" style={s.page}>
      <Text style={s.h1}>Invoice</Text>
      <View style={s.row}><Text>Total</Text><Text>{total}</Text></View>
      <Text style={s.footer} fixed render={({ pageNumber, totalPages }) =>
        `${pageNumber} / ${totalPages}`} />
    </Page>
  </Document>
);
const buffer = await renderToBuffer(<Invoice />);

// After: the same component, returning markup.
import { renderToStaticMarkup } from "react-dom/server";

const Invoice = () => (
  <>
    <h1 className="h1">Invoice</h1>
    <div className="row"><span>Total</span><span>{total}</span></div>
  </>
);

const html = `<!doctype html><html><head><style>${css}</style></head>
  <body>${renderToStaticMarkup(<Invoice />)}</body></html>`;

await fetch("https://api.pdfpipe.xyz/v1/pdf", {
  method: "POST",
  headers: { Authorization: `Bearer ${process.env.PDFPIPE_KEY}`,
             "Content-Type": "application/json" },
  body: JSON.stringify({
    html,
    options: {
      format: "A4",
      margin: { top: "20mm", bottom: "20mm", left: "15mm", right: "15mm" },
      footer_html: '<div style="font-size:8pt"><span class="pageNumber"></span>' +
                   ' / <span class="totalPages"></span></div>',
    },
  }),
});

// The fixed prop with a render function becomes footer_html. The render
// function received the page number; the footer substitutes it into a
// class instead. Anything else that function computed has no equivalent.

The CSS delta, in both directions

What the old engine accepted that a current one does not, and what it refused that a current one wants. The second direction is the one people forget, and it is where the value of the migration is.

  • The StyleSheet object becomes real CSS, which is a considerable expansion. The library supported a subset of flexbox and a handful of box properties; everything else is now available.
  • Every style was inline and scoped to a component. Moving to CSS means selectors, cascade and specificity exist again, which is more power and a new source of mistakes.
  • The library's layout was flexbox-only, so the templates use flex for things a browser would do with normal flow or a table. Those keep working and are worth simplifying afterwards.
  • Real page-break control becomes available. The library's break prop was coarse; break-inside and break-after are not.

What changes without anything erroring

The dangerous list. Each of these produces a different document and no diagnostic, so none of them are caught by a test that only checks the render succeeded.

  • Layout throughout, because the library's flexbox implementation is its own and a browser's is the specification.
  • Page count.
  • Anything the fixed render function computed per page beyond the page number.
  • Font handling, which moves from the library's font registration to @font-face at a public URL.

What to diff before cutting over

Convert one page-worth of components and compare it side by side before converting the rest, because the style translation is the entire migration and doing it once establishes the pattern. Then diff extracted text to confirm nothing was dropped in the component rewrite.

The one thing that always breaks

The fixed elements. They were the library's mechanism for running headers and footers, they are props rather than markup, and they simply stop existing when the component returns HTML. The body renders correctly and every page loses its header, its footer and its page number together.

Frequently asked

Do I have to rewrite my templates to leave @react-pdf/renderer?

Effectively yes, and it is better to plan for that than to discover it. The old tool did not have HTML templates to port, so the document has to be expressed as markup for the first time. The value in the old code is the data layer underneath the drawing, and that part survives untouched.

Can I run both for a while?

Yes, and it is the safest way to do it. Put both behind one internal function that takes your data and returns bytes, switch on an environment variable, and run the new path on real traffic while the old one still serves. You get a diff on real documents rather than on fixtures, and you keep a way back that does not involve a deploy.

What about the documents already generated?

Nothing here changes them. They are files that already exist. What is worth deciding before the cutover is whether a regenerated document has to match the original byte for byte or merely say the same thing, because for anything with a legal or audit character the answer is usually to keep the original file rather than to be able to reproduce it.

Other migrations

The nearest neighbours first, then others that started from the same kind of tool, because the model matters more than the language.

Render one of your existing documents through the playground before changing any code. That comparison is the whole of the risk assessment for this migration.