← Back to blog

22--- title: "Migrating from react-pdf to Forme" description: "A practical guide to switching from react-pdf. What changes, what stays the same, and why page breaks are the reason you're here." date: "2026-02-25"

If you're reading this, you probably hit a wall with react-pdf. Most likely page breaks. Maybe performance. Maybe you needed something that the layout engine just couldn't do.

I've been there. Here's how to move.

Invoice PDF generated with Forme using JSX and React components

The components map

The good news: if you've used react-pdf, Forme will feel familiar. The component model is similar. The differences are in layout behavior and what the engine can actually do.

react-pdf Forme Notes
<Document> <Document> Same
<Page> <Page> Use <Fixed position="header"> and <Fixed position="footer"> children for repeating headers/footers
<View> <View> Same flex layout model
<Text> <Text> Same
<Image> <Image> Same
<Link> href prop Use href prop on <View>, <Text>, <Image>, or <Svg> instead
<Canvas> -- Not available (yet)
<Svg> <Svg> Same. Pass SVG markup via the content prop

The import changes from @react-pdf/renderer to @formepdf/react and the render call changes from renderToBuffer() to renderDocument().

Before and after

react-pdf:

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

const MyDoc = () => (
  <Document>
    <Page size="A4" style={{ padding: 30 }}>
      <View style={{ flexDirection: 'row', marginBottom: 20 }}>
        <Text style={{ fontSize: 24, fontWeight: 'bold' }}>Invoice</Text>
      </View>
      <View style={{ flexDirection: 'row', borderBottom: '1 solid #ccc', paddingBottom: 8 }}>
        <Text style={{ flex: 3, fontSize: 10, color: '#666' }}>Description</Text>
        <Text style={{ flex: 1, fontSize: 10, color: '#666', textAlign: 'right' }}>Amount</Text>
      </View>
      {items.map(item => (
        <View style={{ flexDirection: 'row', paddingVertical: 6 }}>
          <Text style={{ flex: 3, fontSize: 11 }}>{item.name}</Text>
          <Text style={{ flex: 1, fontSize: 11, textAlign: 'right' }}>{item.price}</Text>
        </View>
      ))}
    </Page>
  </Document>
);

const buffer = await renderToBuffer(<MyDoc />);

Forme:

import { Document, Page, View, Text } from '@formepdf/react';
import { renderDocument } from '@formepdf/core';

const MyDoc = () => (
  <Document>
    <Page size="A4" margin={30}>
      <View style={{ flexDirection: 'row', marginBottom: 20 }}>
        <Text style={{ fontSize: 24, fontWeight: 700 }}>Invoice</Text>
      </View>
      <View style={{ flexDirection: 'row', borderBottom: '1px solid #ccc', paddingBottom: 8 }}>
        <Text style={{ flex: 3, fontSize: 10, color: '#666' }}>Description</Text>
        <Text style={{ flex: 1, fontSize: 10, color: '#666', textAlign: 'right' }}>Amount</Text>
      </View>
      {items.map(item => (
        <View style={{ flexDirection: 'row', paddingVertical: 6 }}>
          <Text style={{ flex: 3, fontSize: 11 }}>{item.name}</Text>
          <Text style={{ flex: 1, fontSize: 11, textAlign: 'right' }}>{item.price}</Text>
        </View>
      ))}
    </Page>
  </Document>
);

const pdfBytes = await renderDocument(<MyDoc />);

The changes are small: renderToBuffer becomes renderDocument, fontWeight: 'bold' becomes fontWeight: 700, margin moves from style to a prop on Page. Border shorthands like borderBottom: '1px solid #ccc' work as-is. Most templates migrate in 15-30 minutes.

The reason you're migrating: page breaks

react-pdf has a wrap prop and a break prop. The wrap prop controls whether content can break across pages. Setting wrap={false} on a View means the entire View must fit on one page. If it doesn't fit, react-pdf tries to move it to the next page. If it still doesn't fit, things get unpredictable.

The real problem is flowing content. A long list of items that needs to span multiple pages. react-pdf will break the content, but it doesn't re-render headers, doesn't handle orphaned rows well, and can clip content at page boundaries.

Forme's layout engine was built for this. Content flows across pages automatically. Headers and footers use <Fixed> children inside the Page and repeat on every page. wrap={false} on a View moves the entire View to the next page if it doesn't fit.

<Page size="Letter" margin={[54, 54, 72, 54]}>
  <Fixed position="header">
    <View style={{ flexDirection: 'row', justifyContent: 'space-between', borderBottomWidth: 1, borderBottomColor: '#ddd', paddingBottom: 8 }}>
      <Text style={{ fontSize: 10 }}>Acme Corp</Text>
      <Text style={{ fontSize: 10, color: '#999' }}>Invoice #001</Text>
    </View>
  </Fixed>
  <Fixed position="footer">
    <View style={{ borderTopWidth: 1, borderTopColor: '#ddd', paddingTop: 8 }}>
      <Text style={{ fontSize: 9, color: '#999', textAlign: 'center' }}>
        Page {'{{pageNumber}}'}
      </Text>
    </View>
  </Fixed>

  <Text style={{ fontSize: 24, fontWeight: 700, marginBottom: 20 }}>Invoice</Text>

  {items.map(item => (
    <View wrap={false} style={{ flexDirection: 'row', paddingVertical: 8, borderBottomWidth: 1, borderBottomColor: '#eee' }}>
      <Text style={{ flex: 3 }}>{item.description}</Text>
      <Text style={{ flex: 1, textAlign: 'right' }}>{item.amount}</Text>
    </View>
  ))}
</Page>

100 line items? They flow across as many pages as needed. The header and footer appear on every page. No item gets clipped at a boundary. This is the default behavior, not something you have to configure.

Performance

react-pdf is written in JavaScript. It's doing layout, font parsing, and PDF generation all in JS. For simple documents it's fine. For complex documents with many pages, it slows down.

Forme's engine is Rust compiled to WASM. The layout calculation, font subsetting, and PDF binary generation all happen in compiled code. A 4-page document renders in about 28ms. A 20-page report with tables and images renders in under 200ms.

The difference is most noticeable with repeated renders. If you're generating hundreds of PDFs in a batch job, the per-render overhead matters.

What Forme doesn't have (yet)

Transparency: react-pdf has been around longer. A few things you might miss:

  • Canvas drawing -- react-pdf has a <Canvas> component for custom drawing. Forme doesn't have this yet. (SVG is supported via the <Svg> component.)
  • PDF/A compliance -- not available in Forme yet.

On the other hand, Forme now has features that react-pdf doesn't:

  • Hyphenation -- automatic hyphenation in 35+ languages with hyphens: 'auto'
  • Optimal line breaking -- Knuth-Plass algorithm from TeX, not greedy
  • OpenType shaping -- real ligatures, kerning, and contextual forms
  • BiDi text -- right-to-left Arabic and Hebrew with auto-detection
  • CSS Grid -- 2D grid layout with display: 'grid'

If page breaks, text quality, performance, or the component model are your priorities, Forme is ahead.

The migration checklist

  1. Replace @react-pdf/renderer with @formepdf/react and @formepdf/core
  2. Change renderToBuffer() / renderToStream() to renderDocument()
  3. Move padding/margin on <Page> from style to the margin prop
  4. Replace fontWeight: 'bold' with fontWeight: 700
  5. Border shorthands like borderBottom: '1px solid #ccc' work as-is - no changes needed
  6. Use wrap={false} on Views that shouldn't break across pages
  7. Replace break prop with <PageBreak />
  8. Add <Fixed position="header"> and <Fixed position="footer"> children inside Page for repeating headers/footers
  9. Test your templates -- most will work with minimal changes

The most satisfying part of the migration is deleting whatever page break workaround code you wrote. That code isn't needed anymore.

Forme dev server with hot reload for PDF template development

npm install @formepdf/react @formepdf/core
npm uninstall @react-pdf/renderer

GitHub | Docs