Fix: React PDF Not Working — PDF Not Rendering, Worker Error, or Pages Blank
Part of: React & Frontend Errors
Quick Answer
How to fix react-pdf and @react-pdf/renderer issues — PDF viewer setup, worker configuration, page rendering, text selection, annotations, and generating PDFs in React.
The Problem
The PDF viewer shows a blank area:
import { Document, Page } from 'react-pdf';
function PDFViewer() {
return (
<Document file="/document.pdf">
<Page pageNumber={1} />
</Document>
);
}
// White box — no PDF content visibleOr the worker fails to load:
Error: Setting up fake worker failed
// Or: pdf.worker.mjs not foundOr you’re trying to generate a PDF and it throws:
import { Document, Page, Text } from '@react-pdf/renderer';
// Error: Cannot use import statement in a moduleWhy This Happens
There are two completely different libraries with similar names, both popular, and confusing one for the other accounts for a large share of “react-pdf is broken” reports.
react-pdf(Wojciech Maj) — displays existing PDF files in the browser. It uses PDF.js under the hood and requires a web worker for parsing.@react-pdf/renderer— generates new PDF documents from React components. It creates PDFs programmatically, not a viewer.
The two share no code and have different APIs. If you import { Document, Page, Text } from @react-pdf/renderer and try to display an existing file, nothing renders because that package does not know how to read PDF bytes.
Common issues with react-pdf (the viewer):
- The PDF.js worker must be configured — PDF parsing runs in a web worker for performance. Without the worker, parsing falls back to the main thread (slow) or fails entirely. The error is usually
Setting up fake worker failed. - The
fileprop needs a valid source — a URL, File object, ArrayBuffer, or base64 string. Relative paths must resolve correctly from the browser, and CORS headers must allow the request when loading cross-origin. - CSS text layer and annotation layer need their own CSS imports — without them, text is invisible and links do not work. After v9, the CSS classes were renamed and old imports silently load nothing.
- PDF.js worker ESM vs CJS mismatch — v7+ ships
pdf.worker.min.mjs(ESM). Older bundler configs reach forpdf.worker.min.jsand 404. Webpack 4 and CRA in particular have trouble resolving.mjsfrompdfjs-dist.
Version History That Changes the Failure Mode
The react-pdf package by Wojciech Maj has had three breaking releases in two years. Each shifted the PDF.js dependency forward, and each broke a different class of integrations.
- react-pdf v6.x (early 2023) — ran on PDF.js 3.x. The worker file was
pdf.worker.min.js(CommonJS). Compatible with Webpack 4 and older CRA projects. If you are still on v6, the worker URL recipe ispdfjs-dist/build/pdf.worker.min.js. - react-pdf v7.0 (March 2023) — upgraded PDF.js to 3.4 and switched the worker to ESM (
.mjs). Bundlers that did not handle.mjs(Webpack 4, older Vite configs) started failing with “Cannot find module pdf.worker.min.mjs”. The fix is to either pin v6 or upgrade the bundler. - react-pdf v8.0 (May 2024) — required React 18+ and PDF.js 4.x. Drops support for React 17. The Page component’s prop names for layer rendering stabilized as
renderTextLayerandrenderAnnotationLayer. If you copy v6 examples that passrenderInteractiveForms, that prop is gone. - react-pdf v9.0 (November 2024) — current line. Required PDF.js 4.4+ and React 18.3+. Text layer styles changed: the classes moved from
.react-pdf__Page__textContentto a new selector hierarchy, and the CSS files now live underreact-pdf/dist/Page/TextLayer.css(theesmpath was removed). Custom theming written for v7 may not match. - PDF.js v4.x (2024) — dropped support for IE entirely and requires modern browsers. If your error log says
TypeError: Cannot read properties of undefined (reading 'WorkerMessageHandler'), you have a version skew wherepdfjs-distin node_modules does not match the versionreact-pdfexpects. Pin them together. @react-pdf/rendererv3.x (2023–2024) — the generator’s own major line. v3 introduced better SVG support and improved font loading. Server-side rendering viarenderToBufferandrenderToStreamhas been stable since v2.
If you mix versions — for example react-pdf@9 with a manually pinned pdfjs-dist@3 — the worker handshake silently fails and you see a blank box. Always let react-pdf pull its own pdfjs-dist peer.
Fix 1: PDF Viewer with react-pdf
npm install react-pdf'use client';
import { Document, Page, pdfjs } from 'react-pdf';
import 'react-pdf/dist/Page/AnnotationLayer.css';
import 'react-pdf/dist/Page/TextLayer.css';
import { useState } from 'react';
// Configure the worker — REQUIRED
pdfjs.GlobalWorkerOptions.workerSrc = new URL(
'pdfjs-dist/build/pdf.worker.min.mjs',
import.meta.url,
).toString();
// Or use CDN:
// pdfjs.GlobalWorkerOptions.workerSrc = `//unpkg.com/pdfjs-dist@${pdfjs.version}/build/pdf.worker.min.mjs`;
interface PDFViewerProps {
url: string;
}
function PDFViewer({ url }: PDFViewerProps) {
const [numPages, setNumPages] = useState<number>(0);
const [pageNumber, setPageNumber] = useState(1);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
function onDocumentLoadSuccess({ numPages }: { numPages: number }) {
setNumPages(numPages);
setLoading(false);
}
function onDocumentLoadError(err: Error) {
setError(err.message);
setLoading(false);
}
return (
<div>
{error && <div className="text-red-500">Error: {error}</div>}
<Document
file={url}
onLoadSuccess={onDocumentLoadSuccess}
onLoadError={onDocumentLoadError}
loading={<div>Loading PDF...</div>}
>
<Page
pageNumber={pageNumber}
renderTextLayer={true} // Enable text selection
renderAnnotationLayer={true} // Enable links and annotations
width={800} // Or use scale
// scale={1.5}
/>
</Document>
{!loading && !error && (
<div className="flex items-center gap-4 mt-4">
<button
onClick={() => setPageNumber(p => Math.max(1, p - 1))}
disabled={pageNumber <= 1}
>
Previous
</button>
<span>Page {pageNumber} of {numPages}</span>
<button
onClick={() => setPageNumber(p => Math.min(numPages, p + 1))}
disabled={pageNumber >= numPages}
>
Next
</button>
</div>
)}
</div>
);
}
// Display all pages at once
function AllPagesViewer({ url }: { url: string }) {
const [numPages, setNumPages] = useState(0);
return (
<Document file={url} onLoadSuccess={({ numPages }) => setNumPages(numPages)}>
{Array.from({ length: numPages }, (_, i) => (
<Page key={i + 1} pageNumber={i + 1} width={800} className="mb-4 shadow-lg" />
))}
</Document>
);
}Fix 2: PDF File Sources
// From URL
<Document file="https://example.com/document.pdf" />
// From public directory
<Document file="/documents/report.pdf" />
// From File input
function FileUploadViewer() {
const [file, setFile] = useState<File | null>(null);
return (
<div>
<input
type="file"
accept=".pdf"
onChange={(e) => setFile(e.target.files?.[0] || null)}
/>
{file && (
<Document file={file}>
<Page pageNumber={1} width={600} />
</Document>
)}
</div>
);
}
// From ArrayBuffer / Uint8Array
<Document file={{ data: uint8Array }} />
// From base64
<Document file={`data:application/pdf;base64,${base64String}`} />
// With custom headers (for authenticated endpoints)
<Document
file={{
url: 'https://api.example.com/documents/123',
httpHeaders: { Authorization: `Bearer ${token}` },
}}
/>Fix 3: PDF Generation with @react-pdf/renderer
npm install @react-pdf/renderer// Generate PDFs from React components — completely separate from react-pdf
import {
Document, Page, Text, View, StyleSheet, Image, Link,
Font, PDFDownloadLink, PDFViewer, pdf,
} from '@react-pdf/renderer';
// Register custom fonts
Font.register({
family: 'Inter',
fonts: [
{ src: '/fonts/Inter-Regular.ttf', fontWeight: 400 },
{ src: '/fonts/Inter-Bold.ttf', fontWeight: 700 },
],
});
// Styles — similar to React Native StyleSheet
const styles = StyleSheet.create({
page: {
padding: 40,
fontFamily: 'Inter',
fontSize: 12,
color: '#333',
},
header: {
fontSize: 24,
fontWeight: 700,
marginBottom: 20,
color: '#1a1a2e',
},
section: {
marginBottom: 16,
},
row: {
flexDirection: 'row',
justifyContent: 'space-between',
borderBottomWidth: 1,
borderBottomColor: '#eee',
paddingVertical: 8,
},
total: {
flexDirection: 'row',
justifyContent: 'space-between',
paddingTop: 12,
marginTop: 8,
borderTopWidth: 2,
borderTopColor: '#333',
fontWeight: 700,
fontSize: 14,
},
footer: {
position: 'absolute',
bottom: 30,
left: 40,
right: 40,
textAlign: 'center',
fontSize: 10,
color: '#888',
},
});
// Invoice PDF component
interface InvoiceData {
invoiceNumber: string;
date: string;
items: { description: string; quantity: number; price: number }[];
customerName: string;
}
function InvoicePDF({ data }: { data: InvoiceData }) {
const total = data.items.reduce((sum, item) => sum + item.quantity * item.price, 0);
return (
<Document>
<Page size="A4" style={styles.page}>
{/* Header */}
<View style={{ flexDirection: 'row', justifyContent: 'space-between', marginBottom: 40 }}>
<View>
<Text style={styles.header}>INVOICE</Text>
<Text>Invoice #{data.invoiceNumber}</Text>
<Text>Date: {data.date}</Text>
</View>
<Image src="/logo.png" style={{ width: 100, height: 40 }} />
</View>
{/* Customer */}
<View style={styles.section}>
<Text style={{ fontWeight: 700, marginBottom: 4 }}>Bill To:</Text>
<Text>{data.customerName}</Text>
</View>
{/* Items table */}
<View style={styles.section}>
<View style={[styles.row, { fontWeight: 700, borderBottomWidth: 2 }]}>
<Text style={{ flex: 3 }}>Description</Text>
<Text style={{ flex: 1, textAlign: 'center' }}>Qty</Text>
<Text style={{ flex: 1, textAlign: 'right' }}>Price</Text>
<Text style={{ flex: 1, textAlign: 'right' }}>Total</Text>
</View>
{data.items.map((item, i) => (
<View key={i} style={styles.row}>
<Text style={{ flex: 3 }}>{item.description}</Text>
<Text style={{ flex: 1, textAlign: 'center' }}>{item.quantity}</Text>
<Text style={{ flex: 1, textAlign: 'right' }}>${item.price.toFixed(2)}</Text>
<Text style={{ flex: 1, textAlign: 'right' }}>${(item.quantity * item.price).toFixed(2)}</Text>
</View>
))}
<View style={styles.total}>
<Text>Total</Text>
<Text>${total.toFixed(2)}</Text>
</View>
</View>
{/* Footer */}
<Text style={styles.footer}>Thank you for your business!</Text>
</Page>
</Document>
);
}
// Download button
function DownloadInvoice({ data }: { data: InvoiceData }) {
return (
<PDFDownloadLink document={<InvoicePDF data={data} />} fileName={`invoice-${data.invoiceNumber}.pdf`}>
{({ loading }) => (loading ? 'Generating...' : 'Download Invoice')}
</PDFDownloadLink>
);
}
// Generate on server (API route)
import { renderToBuffer } from '@react-pdf/renderer';
export async function GET() {
const buffer = await renderToBuffer(<InvoicePDF data={invoiceData} />);
return new Response(buffer, {
headers: {
'Content-Type': 'application/pdf',
'Content-Disposition': 'attachment; filename="invoice.pdf"',
},
});
}Fix 4: Zoom, Search, and Thumbnails
'use client';
import { Document, Page, pdfjs } from 'react-pdf';
import { useState } from 'react';
function FullFeaturedViewer({ url }: { url: string }) {
const [numPages, setNumPages] = useState(0);
const [pageNumber, setPageNumber] = useState(1);
const [scale, setScale] = useState(1.0);
return (
<div className="flex gap-4">
{/* Thumbnail sidebar */}
<div className="w-48 overflow-y-auto h-[80vh] bg-gray-100 p-2">
<Document file={url}>
{Array.from({ length: numPages }, (_, i) => (
<div
key={i}
onClick={() => setPageNumber(i + 1)}
className={`cursor-pointer mb-2 ${pageNumber === i + 1 ? 'ring-2 ring-blue-500' : ''}`}
>
<Page pageNumber={i + 1} width={160} renderTextLayer={false} renderAnnotationLayer={false} />
<p className="text-center text-xs">{i + 1}</p>
</div>
))}
</Document>
</div>
{/* Main viewer */}
<div className="flex-1">
<div className="flex items-center gap-2 mb-4">
<button onClick={() => setScale(s => Math.max(0.5, s - 0.25))}>-</button>
<span>{Math.round(scale * 100)}%</span>
<button onClick={() => setScale(s => Math.min(3, s + 0.25))}>+</button>
<button onClick={() => setScale(1)}>Reset</button>
</div>
<Document file={url} onLoadSuccess={({ numPages }) => setNumPages(numPages)}>
<Page pageNumber={pageNumber} scale={scale} />
</Document>
</div>
</div>
);
}Fix 5: Next.js Configuration
// next.config.mjs — handle PDF.js worker
const nextConfig = {
webpack: (config) => {
config.resolve.alias.canvas = false; // Disable canvas for Node.js
return config;
},
};
export default nextConfig;// Dynamic import for client-only rendering
import dynamic from 'next/dynamic';
const PDFViewer = dynamic(() => import('@/components/PDFViewer'), {
ssr: false,
loading: () => <div className="h-96 bg-gray-100 animate-pulse rounded" />,
});
export default function DocumentPage() {
return <PDFViewer url="/documents/report.pdf" />;
}Fix 6: Print PDF
function PrintablePDF({ url }: { url: string }) {
function handlePrint() {
const iframe = document.createElement('iframe');
iframe.style.display = 'none';
iframe.src = url;
document.body.appendChild(iframe);
iframe.onload = () => {
iframe.contentWindow?.print();
setTimeout(() => document.body.removeChild(iframe), 1000);
};
}
return (
<div>
<button onClick={handlePrint}>Print</button>
<Document file={url}>
<Page pageNumber={1} width={800} />
</Document>
</div>
);
}Still Not Working?
Blank page — no PDF visible — the PDF.js worker is not configured. Set pdfjs.GlobalWorkerOptions.workerSrc before rendering any Document component. Without the worker, PDF parsing may silently fail.
“Setting up fake worker failed” — the worker URL is wrong. Use new URL('pdfjs-dist/build/pdf.worker.min.mjs', import.meta.url) for bundler-resolved paths, or the CDN URL for a quick fix. On v6 the file is .js, on v7+ it is .mjs.
Text cannot be selected — import react-pdf/dist/Page/TextLayer.css and set renderTextLayer={true} on the Page component. The text layer overlays invisible selectable text on top of the rendered PDF. On v9 the CSS classes changed, so check that the import path matches your installed version.
@react-pdf/renderer vs react-pdf — these are different libraries. react-pdf displays existing PDFs. @react-pdf/renderer creates new PDFs. Do not mix imports from both.
TypeError: Cannot read properties of undefined (reading 'WorkerMessageHandler') — version skew between react-pdf and pdfjs-dist. Delete node_modules and package-lock.json, then reinstall so the right peer dependency is hoisted.
CORS error loading a PDF from another origin — the bucket or CDN must send Access-Control-Allow-Origin. For S3, configure a CORS policy on the bucket. For private documents, proxy the file through your own API and stream the bytes to avoid the cross-origin handshake.
Page renders tiny in a flex layout — <Page> reads the container width via width or scale. In a flex parent without min-width, it can collapse to zero. Pass an explicit width={containerWidth} from a useResizeObserver value.
SSR error: DOMMatrix is not defined — PDF.js needs a browser environment. In Next.js, load the viewer with dynamic(() => import('./Viewer'), { ssr: false }). Do not try to render <Document> from a Server Component.
For related document and rendering issues, see Fix: MDX Not Working, Fix: pdf-lib Not Working, Fix: Monaco Editor Not Working, and Fix: Tiptap Not Working.
Solo developer based in Japan. Every solution is cross-referenced with official documentation and tested before publishing.
Was this article helpful?
Related Articles
Fix: i18next Not Working — Translations Missing, Language Not Switching, or Namespace Errors
How to fix i18next issues — react-i18next setup, translation file loading, namespace configuration, language detection, interpolation, pluralization, and Next.js integration.
Fix: Mapbox GL JS Not Working — Map Not Rendering, Markers Missing, or Access Token Invalid
How to fix Mapbox GL JS issues — access token setup, React integration with react-map-gl, markers and popups, custom layers, geocoding, directions, and Next.js configuration.
Fix: Million.js Not Working — Compiler Errors, Components Not Optimized, or React Compatibility Issues
How to fix Million.js issues — compiler setup with Vite and Next.js, block() optimization rules, component compatibility constraints, automatic mode, and debugging performance gains.
Fix: Radix UI Not Working — Popover Not Opening, Dialog Closing Immediately, or Styling Breaking
How to fix Radix UI issues — Popover and Dialog setup, controlled vs uncontrolled state, portal rendering, animation with CSS or Framer Motion, accessibility traps, and Tailwind CSS integration.