react-pdf setup: Document and page rendering
Table of contents
react-pdf in a React project. It covers installing the package, configuring the PDF.js web worker, and rendering pages with custom sizing, rotation, and dark mode colors.
react-pdf wraps PDF.js in declarative React components. The minimum setup has three steps:
- Configure the worker — Set
pdfjs.GlobalWorkerOptions.workerSrcat module scope (not inside a component), pointing atpdfjs-dist/build/pdf.worker.min.mjsor a CDN equivalent - Import the CSS —
react-pdf/dist/Page/TextLayer.cssandreact-pdf/dist/Page/AnnotationLayer.cssare required for text selection and link annotations - Render the document — Wrap pages in
<Document file={...}><Page pageNumber={1} /></Document>
The biggest gotcha: The file prop uses === equality. Object literals like { url: "..." } create a new object every render, triggering infinite refetches. Always wrap them in useMemo or useState.
If you’d rather skip the wrapper around a wrapper, Nutrient Web SDK handles worker setup, text selection, annotations, forms, and signatures with one NutrientViewer.load() call.
react-pdf is a React wrapper around PDF.js that provides declarative components for rendering PDFs. It trades lower-level control for a simpler, more React-native API.
Install
Add the package to the project:
npm install react-pdfStep 1: Configure the web worker
PDF.js requires a web worker for parsing.
import { pdfjs } from "react-pdf";
pdfjs.GlobalWorkerOptions.workerSrc = new URL( "pdfjs-dist/build/pdf.worker.min.mjs", import.meta.url,).toString();Alternatively, load the worker from a CDN:
import { pdfjs } from "react-pdf";
pdfjs.GlobalWorkerOptions.workerSrc = `https://unpkg.com/pdfjs-dist@${pdfjs.version}/build/pdf.worker.min.mjs`;Content delivery network (CDN) URLs are convenient for prototyping, but they introduce supply-chain risk, can be blocked by strict Content Security Policy (CSP) rules, and add a third-party dependency on your uptime. For production, copy the worker out of node_modules/pdfjs-dist/build/ with your bundler’s static-copy plugin instead.
Important: Set workerSrc at the top level of the module, not inside a component. Module execution order matters.
Step 2: Import required CSS
Add these imports alongside the component imports:
import "react-pdf/dist/Page/TextLayer.css";import "react-pdf/dist/Page/AnnotationLayer.css";These are required for proper text selection and annotation (link) rendering.
Step 3: Render a PDF
Render a single page with the Document and Page components:
import { useState } from "react";import { Document, Page } from "react-pdf";
function PDFViewer() { const [numPages, setNumPages] = useState(null);
return ( <Document file="https://example.com/document.pdf" onLoadSuccess={({ numPages }) => setNumPages(numPages)} > <Page pageNumber={1} /> </Document> );}The file prop
The file prop accepts several formats:
// URL string.<Document file="https://example.com/doc.pdf" />
// Imported file.import samplePDF from "./sample.pdf";<Document file={samplePDF} />
// File from input.<Document file={selectedFile} /> // File object from <input type="file">.
// `Uint8Array`.<Document file={{ data: uint8Array }} />
// URL object form (alternative to the bare string — also accepts `httpHeaders`/`withCredentials` via the separate `options` prop).<Document file={{ url: "https://example.com/doc.pdf" }} />Critical: The file prop uses === equality checking. If you pass an object literal like file={{ url: "..." }}, it creates a new object every render, causing infinite refetches. Always memoize.
import { useMemo, useState } from "react";
// Bad — creates a new object every render.<Document file={{ url: pdfUrl }} />
// Good — memoized.const file = useMemo(() => ({ url: pdfUrl }), [pdfUrl]);<Document file={file} />
// Also good — stored in state.const [file, setFile] = useState({ url: pdfUrl });<Document file={file} />Rendering all pages
Loop over numPages to render every page in the document:
function AllPages() { const [numPages, setNumPages] = useState(null);
return ( <Document file="document.pdf" onLoadSuccess={({ numPages }) => setNumPages(numPages)} > {numPages && Array.from({ length: numPages }, (_, i) => ( <Page key={`page-${i + 1}`} pageNumber={i + 1} /> ))} </Document> );}Page sizing
Control page dimensions with width, height, or scale:
// Fixed width (height scales proportionally).<Page pageNumber={1} width={600} />
// Fixed height (only used if width is not set).<Page pageNumber={1} height={800} />
// Scale factor (1 = 100%, 1.5 = 150%).<Page pageNumber={1} scale={1.5} />
// Width + scale (width is multiplied by scale).<Page pageNumber={1} width={600} scale={1.5} /> // Renders at 900px.If both width and height are provided, height is ignored.
Rotation
Rotate pages globally or per-page:
// Global rotation (applies to all pages).<Document file={file} rotate={90}> <Page pageNumber={1} /></Document>
// Per-page rotation (overrides document rotation).<Page pageNumber={1} rotate={180} />Valid values: 0, 90, 180, 270.
Document-level scale
Set a default scale for all pages:
<Document file={file} scale={1.5}> <Page pageNumber={1} /> {/* Rendered at 1.5x. */} <Page pageNumber={2} /> {/* Also 1.5x. */}</Document>Per-page scale overrides the document-level value.
Canvas background
Set a custom background color for the canvas:
<Page pageNumber={1} canvasBackground="#f5f5f5" />canvasBackground accepts any valid canvas.fillStyle value.
Page colors (dark mode)
Invert or customize page colors:
<Page pageNumber={1} pageColors={{ background: "#1a1a2e", foreground: "#e0e0e0", }}/>Accessing the canvas element
Pass a canvasRef to get a direct reference to the rendered canvas element:
import { useRef } from "react";
function PageWithCanvasRef() { const canvasRef = useRef(null);
return ( <Page pageNumber={1} canvasRef={canvasRef} onRenderSuccess={() => { // Canvas is ready. console.log(canvasRef.current); // HTMLCanvasElement. }} /> );}Key points
- Configure the worker in the same module where the
react-pdfcomponents are used. - Import both CSS files for text selection and annotations to work.
- Memoize the
fileprop — this is the most commonreact-pdfbug. - Use
widthorscalefor sizing, not CSS transforms on the canvas. Pagemust be a child ofDocument(or receive thepdfprop directly).- Page numbers are 1-indexed (
pageNumber), while page indices are 0-indexed (pageIndex).
FAQ
file prop trigger infinite refetches?react-pdf uses === (referential equality) to decide whether the file prop has changed. An object literal like { url: pdfUrl } is a new object on every render, so react-pdf thinks the file changed, tears down the previous load, and refetches the same PDF. Wrap the object in useMemo or store it in useState so the reference is stable.
pdfjs.GlobalWorkerOptions.workerSrc is a global mutation. If you set it inside a component, the first render of any Document component might fire before your component runs — and you’ll get a “fake worker” warning at best, or a broken render at worst. Setting workerSrc at the top of the module guarantees it’s configured before any PDF.js code runs.
width, height, and scale?width is the rendered pixel width — height scales proportionally. scale is a multiplier against the PDF’s native dimensions. height is only used when neither width nor scale is set. Combining width={600} with scale={1.5} renders at 900 pixels — width is treated as the base and then scaled. If both width and height are supplied, height is ignored.
TextLayer.css styles the invisible text layer that overlays each canvas — without it, text selection visually breaks (selections render at the wrong position or with the wrong color). AnnotationLayer.css styles link annotations and form fields. If you don’t need text selection or links, you can skip the imports, but most apps want both.
build/ worker or legacy/build/?Use pdfjs-dist/build/pdf.worker.min.mjs for modern evergreen browsers. Switch to pdfjs-dist/legacy/build/pdf.worker.min.mjs only if you need to support older browsers (Safari < 15.4, Chrome < 95, or anything pre–top-level await). The legacy build ships polyfills and is slightly larger.
Nutrient replaces the entire setup with one NutrientViewer.load({ container, document }) call. No workerSrc, no CSS imports, no file prop memoization, no Document/Page component tree. Annotations, forms, signatures, and search all work without additional wiring. See the migration guide for switching from react-pdf.
How Nutrient Web SDK handles this
All the worker configuration, CSS imports, and prop memoization shown above is unnecessary with Nutrient Web SDK. Here’s the equivalent setup:
import { useEffect, useRef } from "react";
function PDFViewer({ document }) { const containerRef = useRef(null);
useEffect(() => { const container = containerRef.current; let NutrientViewer = null;
(async () => { NutrientViewer = (await import("@nutrient-sdk/viewer")).default; NutrientViewer.load({ container, document }); })();
return () => { NutrientViewer?.unload(container); }; }, [document]);
return <div ref={containerRef} style={{ height: "100vh" }} />;}The dynamic import() keeps the SDK out of the server-side rendering (SSR) bundle in frameworks like Next.js. NutrientViewer.unload() is idempotent and safe to call even if load() is still in flight — the SDK handles the load-then-unload race internally.
Nutrient’s WebAssembly (WASM) engine handles text selection, annotations, forms, and signatures directly — no canvas-only fallback, no file prop memoization pitfalls, and no separate CSS imports.
See Nutrient Web SDK for a setup with no worker configuration, CSS imports, or memoization gotchas. Follow the migration guide to switch from react-pdf, or talk to Sales about your requirements.