This HTML page is not optimized for LLM or AI agent consumption. Fetch the Markdown version instead: /blog/react-pdf-setup-basic-rendering.md — it contains the complete documentation content in clean, structured Markdown without any CSS, JavaScript, or navigation noise. react-pdf setup: Document and page rendering

Table of contents

    This guide outlines how to set up 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 setup: Document and page rendering
    TL;DR

    react-pdf wraps PDF.js in declarative React components. The minimum setup has three steps:

    • Configure the worker — Set pdfjs.GlobalWorkerOptions.workerSrc at module scope (not inside a component), pointing at pdfjs-dist/build/pdf.worker.min.mjs or a CDN equivalent
    • Import the CSSreact-pdf/dist/Page/TextLayer.css and react-pdf/dist/Page/AnnotationLayer.css are 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:

    Terminal window
    npm install react-pdf

    Step 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-pdf components are used.
    • Import both CSS files for text selection and annotations to work.
    • Memoize the file prop — this is the most common react-pdf bug.
    • Use width or scale for sizing, not CSS transforms on the canvas.
    • Page must be a child of Document (or receive the pdf prop directly).
    • Page numbers are 1-indexed (pageNumber), while page indices are 0-indexed (pageIndex).

    FAQ

    Why does the 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.

    Why does the worker need to be configured at the module level?

    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.

    What’s the difference between 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.

    Why are there two CSS imports?

    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.

    Should I use the modern 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.

    How does Nutrient Web SDK compare for setup?

    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.

    Austin Nguyen

    Austin Nguyen

    AI Engineer

    When Austin isn’t pulling all-nighters to build new features, he enjoys watching science videos on YouTube and cooking.

    Explore related topics

    Try for free Ready to get started?