はじめに
Reactアプリで見積書や契約書、マニュアルなどのPDFをプレビュー表示したくなったことはありませんか。手っ取り早い方法として <iframe src="document.pdf" /> を使う人は多いと思いますが、ページ番号の取得やズーム操作、テキスト選択のカスタマイズができず、結局「PDFの中身はブラウザ任せ」という状態から抜け出せずに困っていました。
そこで出会ったのが react-pdf です。PDFの各ページをReactコンポーネントとして扱えるため、ページネーションやズーム、テキストレイヤーの制御まで、すべて自分のコードでハンドリングできるようになりました。この記事では、react-pdfの特徴から実装例までを紹介します。
react-pdfとは
react-pdf は、MozillaのPDF描画エンジンである PDF.js をラップし、PDFファイルをReactコンポーネントとして表示できるようにするライブラリです。「PDFを画像と同じくらい簡単に表示する」というコンセプトのもと、GitHub上で個人開発者のwojtekmaj氏によって継続的にメンテナンスされています。
主な特徴
- 宣言的なAPI -
<Document>と<Page>という2つのコンポーネントだけでPDFを表示できる - 多様な入力形式に対応 - URL、Base64、Uint8Arrayなど複数のソースからPDFを読み込める
- テキスト選択・リンクに対応 - テキストレイヤーとアノテーションレイヤーにより、PDF内のテキスト選択やリンククリックが可能
- TypeScript対応 - 型定義が同梱されており、型安全に利用できる
インストール
npm install react-pdf
yarn add react-pdf
react-pdfはPDF.jsのWebWorkerを利用するため、Worker用のスクリプトを読み込む設定も併せて行います。
import { pdfjs } from 'react-pdf';
pdfjs.GlobalWorkerOptions.workerSrc = new URL(
'pdfjs-dist/build/pdf.worker.min.mjs',
import.meta.url,
).toString();
テキスト選択や注釈リンクを使う場合は、専用のCSSも読み込んでおきましょう。
import 'react-pdf/dist/Page/AnnotationLayer.css';
import 'react-pdf/dist/Page/TextLayer.css';
基本的な使い方
もっともシンプルな例として、1つのPDFファイルの1ページ目を表示するコンポーネントを作ってみます。
import { useState } from 'react';
import { Document, Page, pdfjs } from 'react-pdf';
import 'react-pdf/dist/Page/AnnotationLayer.css';
import 'react-pdf/dist/Page/TextLayer.css';
pdfjs.GlobalWorkerOptions.workerSrc = new URL(
'pdfjs-dist/build/pdf.worker.min.mjs',
import.meta.url,
).toString();
function PdfViewer({ fileUrl }) {
const [numPages, setNumPages] = useState(null);
const [pageNumber, setPageNumber] = useState(1);
function onDocumentLoadSuccess({ numPages }) {
setNumPages(numPages);
setPageNumber(1);
}
return (
<div>
<Document file={fileUrl} onLoadSuccess={onDocumentLoadSuccess}>
<Page pageNumber={pageNumber} />
</Document>
{numPages && (
<p>
ページ {pageNumber} / {numPages}
</p>
)}
</div>
);
}
export default PdfViewer;
Document の file propにPDFのURLやFileオブジェクトを渡すだけで、内部でPDF.jsが解析・描画を行ってくれます。onLoadSuccess コールバックから総ページ数(numPages)が取得できるので、これを使ってページ送りのUIを組み立てられます。
なお、react-pdfはブラウザのCanvas APIに依存しているため、Next.jsなどでSSRを行うフレームワークでは動的インポートを使い、クライアントサイドのみで読み込むようにしてください。
'use client';
import dynamic from 'next/dynamic';
const PdfViewer = dynamic(() => import('./PdfViewer'), { ssr: false });
実践的なユースケース
契約書ビューアのように、ページ送りとズーム操作ができる実用的なコンポーネントを作ってみます。
import { useState } from 'react';
import { Document, Page } from 'react-pdf';
function ContractViewer({ fileUrl }) {
const [numPages, setNumPages] = useState(0);
const [pageNumber, setPageNumber] = useState(1);
const [scale, setScale] = useState(1.0);
function onDocumentLoadSuccess({ numPages }) {
setNumPages(numPages);
}
function goToPrevPage() {
setPageNumber((prev) => Math.max(prev - 1, 1));
}
function goToNextPage() {
setPageNumber((prev) => Math.min(prev + 1, numPages));
}
return (
<div>
<div style={{ marginBottom: 8 }}>
<button onClick={goToPrevPage} disabled={pageNumber <= 1}>
前のページ
</button>
<span style={{ margin: '0 8px' }}>
{pageNumber} / {numPages}
</span>
<button onClick={goToNextPage} disabled={pageNumber >= numPages}>
次のページ
</button>
<button onClick={() => setScale((s) => Math.min(s + 0.2, 2))}>
拡大
</button>
<button onClick={() => setScale((s) => Math.max(s - 0.2, 0.4))}>
縮小
</button>
</div>
<Document file={fileUrl} onLoadSuccess={onDocumentLoadSuccess}>
<Page pageNumber={pageNumber} scale={scale} />
</Document>
</div>
);
}
export default ContractViewer;
scale propを状態として持たせるだけでズームイン・アウトが実装できますし、pageNumber を切り替えるだけでページ送りが完成します。すべてがReactコンポーネントとpropsの世界で完結するため、既存のUIコンポーネント(ボタンやスライダーなど)とも自然に組み合わせられるのが大きな利点です。
サムネイル一覧が欲しい場合は、<Page> を numPages の数だけ小さいスケールでループ表示すれば、簡易的なドキュメントギャラリーも実現できます。
まとめ
react-pdfを使うことで、iframeに頼らずともPDFをReactコンポーネントとして自在に扱えるようになります。ページネーションやズーム、テキスト選択といった機能を自前のUIに組み込めるため、契約書ビューアや資料プレビューなど、業務システムにPDF表示機能を組み込みたい場面で特に力を発揮します。
Worker周りの設定だけ最初につまずきやすいポイントですが、一度セットアップしてしまえばあとは <Document> と <Page> を組み合わせるだけです。PDF表示に課題を感じている方は、ぜひ一度試してみてください。
