Skip to content

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

FixDevs · (Updated: )

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 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, 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 file prop 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 for pdf.worker.min.js and 404. Webpack 4 and CRA in particular have trouble resolving .mjs from pdfjs-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 is pdfjs-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 renderTextLayer and renderAnnotationLayer. If you copy v6 examples that pass renderInteractiveForms, 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__textContent to a new selector hierarchy, and the CSS files now live under react-pdf/dist/Page/TextLayer.css (the esm path 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 where pdfjs-dist in node_modules does not match the version react-pdf expects. Pin them together.
  • @react-pdf/renderer v3.x (2023–2024) — the generator’s own major line. v3 introduced better SVG support and improved font loading. Server-side rendering via renderToBuffer and renderToStream has 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.

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