NaN/NaN/NaN

ReactアプリでPDFをダウンロードさせたい、請求書や証明書をPDFで出力したいというニーズは非常に多くあります。しかし「どのライブラリを使えばいいのか」と迷うことも多いでしょう。npm上にはReact向けのPDF生成ライブラリが複数存在し、それぞれに異なる特徴と制限があります。

この記事では、主要なReact向けPDFライブラリを実際のコード例とともに比較し、プロジェクトに合った選択肢を見つけるお手伝いをします。API方式(FUNBREW PDF)との比較も含め、2026年時点での最適解を解説します。

フロントエンドフレームワーク全般でのPDF API活用についてはNext.js・Nuxt 3でPDF APIを使う完全ガイド、PDF生成全般の概念はHTML→PDF変換 完全ガイドを参照してください。

ReactでPDFを生成する主な方法

ReactでPDFを生成する手段は大きく3つに分類できます。

分類 代表的なライブラリ/手段 動作場所
JSXベースのPDF描画 @react-pdf/renderer クライアント or Node.js
DOM→PDF変換 jsPDF + html2canvas クライアント
PDF表示(Viewer) react-pdf(pdfjs-dist) クライアント
API方式 FUNBREW PDF サーバーサイド

それぞれを詳しく見ていきましょう。

1. react-pdf(pdfjs-distのReactラッパー)

GitHubリポジトリ: wojtekmaj/react-pdf

まず注意が必要なのは、npmで「react-pdf」と検索すると2種類のパッケージがヒットすることです。

  • react-pdf(wojtekmaj製): PDF.js(Mozilla)のReactラッパー。PDF 表示のためのビューワーライブラリ
  • @react-pdf/renderer(diegomura製): PDFを生成するためのライブラリ

ここでは前者のreact-pdf(ビューワー)を紹介し、後者の@react-pdf/renderer(ジェネレーター)は次のセクションで解説します。

react-pdf(ビューワー)の特徴

npm install react-pdf
import { Document, Page, pdfjs } from 'react-pdf';
import 'react-pdf/dist/Page/AnnotationLayer.css';
import 'react-pdf/dist/Page/TextLayer.css';

// PDF.jsのWorkerを設定
pdfjs.GlobalWorkerOptions.workerSrc = new URL(
  'pdfjs-dist/build/pdf.worker.min.mjs',
  import.meta.url,
).toString();

export function PdfViewer({ fileUrl }: { fileUrl: string }) {
  const [numPages, setNumPages] = useState<number>(0);

  function onDocumentLoadSuccess({ numPages }: { numPages: number }) {
    setNumPages(numPages);
  }

  return (
    <Document file={fileUrl} onLoadSuccess={onDocumentLoadSuccess}>
      {Array.from(new Array(numPages), (_, index) => (
        <Page key={`page_${index + 1}`} pageNumber={index + 1} />
      ))}
    </Document>
  );
}

向いているケース:

  • サーバーから取得したPDFをアプリ内でプレビュー表示したい
  • 既存のPDFファイルを読み込んでページ操作したい

向いていないケース:

  • PDF自体を新規に生成したい(このライブラリには生成機能がない)

2. @react-pdf/renderer(JSXでPDFを構築)

GitHubリポジトリ: diegomura/react-pdf

@react-pdf/rendererはReactのJSX構文でPDFのレイアウトを定義し、PDF Binaryを生成するライブラリです。HTML/CSSをPDFに変換するのではなく、独自のコンポーネント(<Document>, <Page>, <Text>, <View>など)を組み合わせてPDFを構築します。

インストールと基本的な使い方

npm install @react-pdf/renderer
import {
  Document,
  Page,
  Text,
  View,
  StyleSheet,
  PDFDownloadLink,
  Font,
} from '@react-pdf/renderer';

// スタイル定義(CSS-in-JSに似た記法)
const styles = StyleSheet.create({
  page: {
    flexDirection: 'column',
    backgroundColor: '#ffffff',
    padding: 40,
  },
  header: {
    fontSize: 24,
    marginBottom: 20,
    color: '#1a56db',
  },
  table: {
    display: 'flex',
    flexDirection: 'column',
    width: '100%',
  },
  tableRow: {
    flexDirection: 'row',
    borderBottomWidth: 1,
    borderBottomColor: '#e5e7eb',
    paddingVertical: 8,
  },
  tableCell: {
    flex: 1,
    fontSize: 10,
  },
});

// PDF ドキュメント定義
const InvoiceDocument = ({
  invoiceNumber,
  customerName,
  items,
  total,
}: {
  invoiceNumber: string;
  customerName: string;
  items: Array<{ name: string; quantity: number; price: number }>;
  total: number;
}) => (
  <Document>
    <Page size="A4" style={styles.page}>
      <Text style={styles.header}>請求書 #{invoiceNumber}</Text>
      <Text style={{ fontSize: 12, marginBottom: 20 }}>
        請求先: {customerName} 様
      </Text>
      <View style={styles.table}>
        {items.map((item, i) => (
          <View key={i} style={styles.tableRow}>
            <Text style={styles.tableCell}>{item.name}</Text>
            <Text style={styles.tableCell}>{item.quantity}</Text>
            <Text style={styles.tableCell}>
              ¥{item.price.toLocaleString()}
            </Text>
          </View>
        ))}
      </View>
      <Text style={{ fontSize: 14, fontWeight: 'bold', marginTop: 20, textAlign: 'right' }}>
        合計: ¥{total.toLocaleString()}
      </Text>
    </Page>
  </Document>
);

// ダウンロードリンクとしてレンダリング
export function InvoiceDownloadButton({
  invoiceData,
}: {
  invoiceData: {
    invoiceNumber: string;
    customerName: string;
    items: Array<{ name: string; quantity: number; price: number }>;
    total: number;
  };
}) {
  return (
    <PDFDownloadLink
      document={<InvoiceDocument {...invoiceData} />}
      fileName={`invoice-${invoiceData.invoiceNumber}.pdf`}
    >
      {({ loading }) => (loading ? '生成中...' : 'PDFをダウンロード')}
    </PDFDownloadLink>
  );
}

日本語フォントの設定

@react-pdf/rendererでデフォルトのフォントのまま日本語を表示すると、文字が豆腐(□)になります。日本語を正しく表示するには、日本語フォントを登録する必要があります。

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

// Google Fonts から Noto Sans JP を登録
Font.register({
  family: 'NotoSansJP',
  src: 'https://fonts.gstatic.com/s/notosansjp/v52/-F6jfjtqLzI2JPCgQBnw7HFyzSD-AsregP8VFBEj75s.woff2',
});

const styles = StyleSheet.create({
  page: {
    fontFamily: 'NotoSansJP', // 登録したフォントを適用
    padding: 40,
  },
});

ただし、外部URLからフォントを読み込むためネットワーク環境に依存します。フォントファイルをローカルに持つ場合は、public/fonts/に配置してパスを指定します。

@react-pdf/rendererの制限事項

  • 独自コンポーネントのみ: 既存のHTML/JSXコンポーネントを直接PDFにできない。専用コンポーネントで書き直す必要がある
  • CSSの部分サポート: FlexboxやBasic Typographyは動くが、CSS GridやBoxShadow、複雑なアニメーションは非対応
  • 日本語には追加設定が必要: フォント登録がないと文字化けする
  • ファイルサイズが増加しやすい: フォントを埋め込むとバンドルサイズが大きくなる
  • 複雑なレイアウトの限界: 表や多段組みなど複雑なレイアウトの実装が手間

3. jsPDF + html2canvas

GitHubリポジトリ: parallax/jsPDF, niklasvh/html2canvas

jsPDFはPDFを直接生成するライブラリで、html2canvasと組み合わせてDOMをスクリーンショットしてPDFに貼り付ける手法がよく使われています。

インストールと基本的な使い方

npm install jspdf html2canvas
import jsPDF from 'jspdf';
import html2canvas from 'html2canvas';
import { useRef } from 'react';

export function ReportExporter() {
  const reportRef = useRef<HTMLDivElement>(null);

  const handleExport = async () => {
    if (!reportRef.current) return;

    try {
      // DOMをCanvasに変換
      const canvas = await html2canvas(reportRef.current, {
        scale: 2,              // 高解像度(Retina対応)
        useCORS: true,         // 外部リソースの読み込みを許可
        logging: false,
        backgroundColor: '#ffffff',
      });

      // CanvasをPDFに変換
      const imgData = canvas.toDataURL('image/png');
      const pdf = new jsPDF({
        orientation: 'portrait',
        unit: 'mm',
        format: 'a4',
      });

      const pdfWidth = pdf.internal.pageSize.getWidth();
      const pdfHeight = pdf.internal.pageSize.getHeight();
      const canvasWidth = canvas.width;
      const canvasHeight = canvas.height;
      const ratio = canvasWidth / pdfWidth;
      const imgHeight = canvasHeight / ratio;

      // 1ページに収まる場合
      if (imgHeight <= pdfHeight) {
        pdf.addImage(imgData, 'PNG', 0, 0, pdfWidth, imgHeight);
      } else {
        // 複数ページにまたがる場合(手動で分割)
        let remainingHeight = imgHeight;
        let yPosition = 0;

        while (remainingHeight > 0) {
          pdf.addImage(imgData, 'PNG', 0, yPosition, pdfWidth, imgHeight);
          remainingHeight -= pdfHeight;
          yPosition -= pdfHeight;
          if (remainingHeight > 0) {
            pdf.addPage();
          }
        }
      }

      pdf.save('report.pdf');
    } catch (error) {
      console.error('PDF生成エラー:', error);
    }
  };

  return (
    <div>
      <div ref={reportRef} style={{ padding: '40px', background: '#fff' }}>
        <h1>月次レポート</h1>
        <p>2026年3月のサマリーです。</p>
        {/* レポートのコンテンツ */}
      </div>
      <button onClick={handleExport}>PDFでエクスポート</button>
    </div>
  );
}

jsPDF + html2canvasの制限事項

この手法はシンプルに見えますが、実際の運用では多くの制限にぶつかります。

テキストが画像になる: html2canvasはDOMをスクリーンショットするため、PDFの中のテキストは実際には画像です。コピー&ペーストができず、検索にもヒットしません。

ページ分割が難しい: 長いコンテンツを複数ページに自動分割する機能がなく、要素の途中でページが切れることがあります。

CSSの再現性が低い: position: fixedoverflow: hidden、複雑なグラデーションなどはhtml2canvasで正確に再現されないことがあります。

解像度の問題: scale: 2を指定しないとぼけたPDFになります。スケールを上げるとメモリ消費が増加します。

日本語の扱い: フォントがシステムフォントに依存するため、環境によって表示が異なります。

// よくある問題:ページ途中で要素が切れる
// 解決策として要素に改ページヒントを追加する方法があるが、完全ではない
const style = {
  pageBreakInside: 'avoid' as const,  // 一部のブラウザで効かない
  breakInside: 'avoid' as const,
};

jsPDFで直接テキストを描画する方法

画像に頼らず、jsPDFのAPIで直接テキストや図形を描画することもできます。テキストはコピー可能になりますが、実装は複雑です。

import jsPDF from 'jspdf';

function generateSimpleInvoice() {
  const doc = new jsPDF();

  // フォント設定(日本語には追加対応が必要)
  doc.setFont('helvetica', 'bold');
  doc.setFontSize(20);
  doc.text('Invoice #INV-001', 20, 20);

  doc.setFont('helvetica', 'normal');
  doc.setFontSize(12);
  doc.text('Customer: ACME Corp', 20, 35);
  doc.text('Date: 2026-04-05', 20, 45);

  // 区切り線
  doc.setLineWidth(0.5);
  doc.line(20, 55, 190, 55);

  // テーブル(手動で座標計算)
  const headers = ['Item', 'Qty', 'Price', 'Total'];
  const colWidths = [80, 25, 35, 35];
  let x = 20;
  doc.setFont('helvetica', 'bold');
  doc.setFontSize(10);
  headers.forEach((header, i) => {
    doc.text(header, x, 65);
    x += colWidths[i];
  });

  // 日本語を使いたい場合は別途フォントを追加する必要がある
  // doc.addFont('NotoSansJP.ttf', 'NotoSansJP', 'normal');
  // doc.setFont('NotoSansJP');

  doc.save('invoice.pdf');
}

日本語を使うにはTTFフォントファイルをBase64エンコードしてjsPDFに登録する必要があり、バンドルサイズが大幅に増えます。

4. API方式:FUNBREW PDF

これまで紹介したクライアントサイドライブラリとは異なり、FUNBREW PDFはサーバーサイドでHTMLをChromiumエンジンでレンダリングしてPDFを生成するAPIサービスです。

考え方の転換: PDF生成を「クライアントサイドで完結させる」ではなく、「サーバーサイドのAPIに任せる」という方式です。

基本的な仕組み

Reactアプリ → API Route(Next.js等) → FUNBREW PDF API → PDF → ダウンロード

クライアントからAPIキーを直接呼び出すのではなく、Next.jsのAPI RouteやNuxt 3のServer Routeを経由します。

Next.jsでの実装例

npm install @funbrew/pdf
// app/api/generate-pdf/route.ts(Next.js App Router)
import { NextRequest, NextResponse } from 'next/server';
import { FunbrewPdf } from '@funbrew/pdf';

const client = new FunbrewPdf({
  apiKey: process.env.FUNBREW_PDF_API_KEY!,
});

export async function POST(request: NextRequest) {
  const { invoiceData } = await request.json();

  // 既存のHTMLテンプレートをそのまま活用できる
  const html = `<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="UTF-8">
  <style>
    @import url('https://fonts.googleapis.com/css2?family=Noto+Sans+JP&display=swap');
    body {
      font-family: 'Noto Sans JP', sans-serif;
      padding: 40px;
      color: #111;
    }
    .header { display: flex; justify-content: space-between; margin-bottom: 30px; }
    h1 { color: #1a56db; font-size: 28px; }
    table { width: 100%; border-collapse: collapse; }
    th, td { border: 1px solid #e5e7eb; padding: 12px; text-align: left; }
    th { background: #f9fafb; font-weight: 600; }
    .total-row td { font-weight: bold; background: #f0f9ff; }
  </style>
</head>
<body>
  <div class="header">
    <h1>請求書 #${invoiceData.invoiceNumber}</h1>
    <div>
      <p>発行日: ${invoiceData.date}</p>
      <p>支払期限: ${invoiceData.dueDate}</p>
    </div>
  </div>
  <p>請求先: <strong>${invoiceData.customerName}</strong> 様</p>
  <table>
    <thead>
      <tr><th>品目</th><th>数量</th><th>単価</th><th>小計</th></tr>
    </thead>
    <tbody>
      ${invoiceData.items.map((item: { name: string; quantity: number; price: number }) => `
        <tr>
          <td>${item.name}</td>
          <td>${item.quantity}</td>
          <td>¥${item.price.toLocaleString()}</td>
          <td>¥${(item.quantity * item.price).toLocaleString()}</td>
        </tr>
      `).join('')}
    </tbody>
    <tfoot>
      <tr class="total-row">
        <td colspan="3">合計(税込)</td>
        <td>¥${invoiceData.total.toLocaleString()}</td>
      </tr>
    </tfoot>
  </table>
</body>
</html>`;

  const pdfBuffer = await client.generate({
    html,
    options: {
      format: 'A4',
      margin: { top: '20mm', bottom: '20mm', left: '15mm', right: '15mm' },
    },
  });

  return new NextResponse(pdfBuffer, {
    headers: {
      'Content-Type': 'application/pdf',
      'Content-Disposition': `attachment; filename="invoice-${invoiceData.invoiceNumber}.pdf"`,
    },
  });
}
// components/InvoiceDownloadButton.tsx(クライアントコンポーネント)
'use client';

import { useState } from 'react';

interface InvoiceData {
  invoiceNumber: string;
  customerName: string;
  date: string;
  dueDate: string;
  items: Array<{ name: string; quantity: number; price: number }>;
  total: number;
}

export function InvoiceDownloadButton({
  invoiceData,
}: {
  invoiceData: InvoiceData;
}) {
  const [loading, setLoading] = useState(false);

  const handleDownload = async () => {
    setLoading(true);
    try {
      const response = await fetch('/api/generate-pdf', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ invoiceData }),
      });

      if (!response.ok) throw new Error('PDF生成失敗');

      const blob = await response.blob();
      const url = URL.createObjectURL(blob);
      const a = document.createElement('a');
      a.href = url;
      a.download = `invoice-${invoiceData.invoiceNumber}.pdf`;
      document.body.appendChild(a);
      a.click();
      document.body.removeChild(a);
      URL.revokeObjectURL(url);
    } catch (error) {
      console.error(error);
      alert('ダウンロードに失敗しました');
    } finally {
      setLoading(false);
    }
  };

  return (
    <button
      onClick={handleDownload}
      disabled={loading}
      className="px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700 disabled:opacity-50"
    >
      {loading ? '生成中...' : 'PDFをダウンロード'}
    </button>
  );
}

cURLで直接呼び出す場合

curl -X POST https://api.pdf.funbrew.cloud/v1/pdf/from-html \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "html": "<h1 style=\"font-family: sans-serif;\">請求書</h1><p>合計: ¥10,000</p>",
    "engine": "quality",
    "format": "A4",
    "margin": {
      "top": "20mm",
      "bottom": "20mm",
      "left": "15mm",
      "right": "15mm"
    }
  }' \
  -o invoice.pdf

プレイグラウンドでHTMLを入力してすぐに試すことができます。

主要ライブラリ比較表

機能 @react-pdf/renderer jsPDF + html2canvas FUNBREW PDF(API)
テキストのコピー可能 ✗(画像化)
CSS完全対応 △(独自CSS) △(再現性に限界) ◎(Chromiumレンダリング)
日本語対応 △(要フォント設定) △(環境依存) ◎(プリインストール済み)
既存HTMLを流用 ✗(専用コンポーネント要) ◎(DOMをそのまま) ◎(HTMLをそのまま送信)
ページ自動分割 △(手動実装が必要)
ヘッダー/フッター
ページ番号
印刷CSS対応
バンドルサイズ △(重い) △(フォント含めると重い) ◎(API呼び出しのみ)
サーバーサイド動作
APIキー不要 △(APIキー必要)
コスト 無料(OSS) 無料(OSS) 無料〜有料(プランあり)
セキュリティ △(クライアント実行) △(クライアント実行) ◎(サーバーサイド)

パフォーマンス比較

指標 @react-pdf/renderer jsPDF + html2canvas FUNBREW PDF
初期バンドルサイズ(概算) ~300KB(gz後) ~200KB(gz後) 最小限(fetch呼び出し)
A4 1ページの生成時間(目安) 500ms〜2s 1s〜5s(DOM複雑度による) 1s〜3s(ネットワーク含む)
複雑なレイアウトへの対応 限定的 低い 高い
並行処理 ブラウザのリソース依存 ブラウザのリソース依存 API側でスケーリング

ユースケース別おすすめ

シンプルなドキュメント生成(CSVエクスポートの延長線)

jsPDFで直接テキスト・表を描画する方法が最も軽量です。日本語が不要で、シンプルな構造なら十分機能します。

// シンプルなテーブルデータのエクスポートには jsPDF で十分
import jsPDF from 'jspdf';
import 'jspdf-autotable';

const doc = new jsPDF();
(doc as any).autoTable({
  head: [['Name', 'Score', 'Date']],
  body: [
    ['Alice', '95', '2026-04-01'],
    ['Bob', '87', '2026-04-02'],
  ],
});
doc.save('scores.pdf');

Reactコンポーネントから静的なPDFを生成したい

デザインを完全にコントロールしたく、既存のReactコンポーネントとは別にPDFレイアウトを定義してもよい場合は、@react-pdf/rendererが適しています。ただし日本語フォントの設定とCSS制限は要注意です。

既存のHTML/CSSをそのままPDFにしたい

Reactコンポーネントで作ったUI(請求書テンプレート、証明書など)をそのままPDFにしたい場合は、FUNBREW PDFのAPI方式が最も確実です。既存のHTMLとCSSをAPI呼び出しに渡すだけで、Chromiumが正確にレンダリングします。

認証が必要なデータを含む機密ドキュメント

ユーザーごとに内容が変わる請求書、給与明細、医療記録などはクライアントサイドでの生成はセキュリティリスクがあります。サーバーサイドAPIで生成し、認証済みエンドポイント経由で配信するのが安全です。

大量の一括PDF生成

月次請求書を100件まとめて生成するようなバッチ処理は、クライアントサイドライブラリには不向きです。API方式ならサーバーサイドで並列処理でき、ブラウザのリソースを消費しません。詳細はPDF一括生成ガイドをご参照ください。

プロトタイプ・個人開発

外部APIへの依存をなくしたい、コストをかけたくないという場合は@react-pdf/rendererjsPDFで始め、要件が複雑になってきたらAPI方式に移行するのが現実的な選択です。

よくある移行パターン

@react-pdf/rendererからAPI方式への移行

@react-pdf/rendererで構築したPDFのデザインをHTMLに変換し、API方式に移行するパターンです。

// Before: @react-pdf/renderer
const InvoicePdf = () => (
  <Document>
    <Page style={{ padding: 40 }}>
      <View style={{ flexDirection: 'row', justifyContent: 'space-between' }}>
        <Text style={{ fontSize: 24, color: '#1a56db' }}>請求書 #001</Text>
        <Text style={{ fontSize: 12 }}>2026-04-05</Text>
      </View>
    </Page>
  </Document>
);

// After: API方式(HTMLテンプレート)
const invoiceHtml = `
  <div style="display: flex; justify-content: space-between; align-items: center;">
    <h1 style="font-size: 24px; color: #1a56db;">請求書 #001</h1>
    <span style="font-size: 12px;">2026-04-05</span>
  </div>
`;

// → このHTMLをAPI Routeに渡してFUNBREW PDFで生成

既存のCSSスタイルをそのまま使えるため、デザインの移植コストが低くなります。

jsPDF + html2canvasからAPI方式への移行

// Before: html2canvas + jsPDF
const handleExport = async () => {
  const canvas = await html2canvas(reportRef.current);
  const pdf = new jsPDF();
  pdf.addImage(canvas.toDataURL(), 'PNG', 0, 0);
  pdf.save('report.pdf');
};

// After: API方式(テキストがコピー可能になる)
const handleExport = async () => {
  const htmlContent = reportRef.current?.outerHTML ?? '';
  const response = await fetch('/api/generate-pdf', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ html: htmlContent }),
  });
  const blob = await response.blob();
  // ... ダウンロード処理
};

テキストが画像化されなくなり、PDFの品質が大幅に向上します。

まとめ

ReactでのPDF生成ライブラリ選択のポイントをまとめます。

  • react-pdf(wojtekmaj): PDF表示専用。生成には使えない
  • @react-pdf/renderer: JSXでPDFをゼロから構築。デザイン制御は高いが、既存HTMLは流用できず日本語対応が手間
  • jsPDF + html2canvas: DOMをスクリーンショットしてPDFに変換。実装は簡単だが、テキストが画像化されページ分割が難しい
  • FUNBREW PDF(API方式): 既存HTMLをそのままAPI経由でPDFに変換。CSS/日本語/ページ分割が確実で、セキュリティも高い

単純なドキュメントやプロトタイプ段階ではOSSライブラリで始め、品質・セキュリティ・保守性が重要になったらAPI方式への移行を検討するのが現実的なアプローチです。

プレイグラウンドでHTMLを入力してFUNBREW PDFの動作をすぐに確認できます。ドキュメントにはAPIリファレンスと各言語のクイックスタートが掲載されています。その他のユースケースはユースケース一覧もご覧ください。

関連記事

Powered by FUNBREW PDF