Fix: React PDF Not Working — PDF Not Rendering, Worker Error, or Pages Blank
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:
react-pdf— 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.
Common issues with react-pdf (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
fileprop needs a valid source — a URL, File object, ArrayBuffer, or base64 string. Relative paths must resolve correctly from the browser. - CSS text layer and annotation layer need their own CSS imports — without them, text is invisible and links don’t work.
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 isn’t 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.
Text can’t 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.
@react-pdf/renderer vs react-pdf — these are different libraries. react-pdf displays existing PDFs. @react-pdf/renderer creates new PDFs. Don’t mix imports from both.
For related document and rendering issues, see Fix: MDX Not Working and Fix: Next.js App Router 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.