Skip to content

Fix: React PDF Not Working — PDF Not Rendering, Worker Error, or Pages Blank

FixDevs ·

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 visible

Or the worker fails to load:

Error: Setting up fake worker failed
// Or: pdf.worker.mjs not found

Or 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 module

Why 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 file prop 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.

F

FixDevs

Solo developer based in Japan. Every solution is cross-referenced with official documentation and tested before publishing.

Was this article helpful?

Related Articles