惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

GbyAI
GbyAI
人人都是产品经理
人人都是产品经理
Hugging Face - Blog
Hugging Face - Blog
罗磊的独立博客
博客园 - 【当耐特】
D
Docker
Y
Y Combinator Blog
L
LangChain Blog
博客园 - 三生石上(FineUI控件)
I
InfoQ
阮一峰的网络日志
阮一峰的网络日志
F
Fortinet All Blogs
J
Java Code Geeks
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
V
V2EX
B
Blog
The GitHub Blog
The GitHub Blog
腾讯CDC
MongoDB | Blog
MongoDB | Blog
博客园 - Franky
爱范儿
爱范儿
A
About on SuperTechFans
量子位
Cyber Security Advisories - MS-ISAC
Cyber Security Advisories - MS-ISAC

Inside Nutrient

A guide to the invisible work behind documents Introducing Nutrient Documents for Salesforce: Native document generation and signing Document AI vs. traditional OCR: Choosing between OCR, AI, and hybrid pipelines PDF SDK compliance and security evaluation checklist for enterprise teams (2026) Invariant Corp replaces paper processes with Nutrient Workflow and scales without limits What is process mapping? A complete guide Nutrient vs. Conga Composer for Salesforce document generation (2026) Document routing: How to automate document distribution The CTO’s AI playbook: Why accountability architecture beats orchestration Compliance workflow automation: Why built-in compliance is table stakes Workflow diagrams: Examples, symbols, and how to build one that actually runs Digital forms: Replace paper forms with automated workflows Approval workflow software: How to automate approvals Why document-centric automation is different The CEO’s AI playbook: Why decision architecture beats model selection Nutrient SDK product updates for Q1 2026 PDF redaction verification: How to prove sensitive data is permanently removed What is a VPAT? The complete guide to accessibility conformance reports What is PDF/UA? The accessible PDF standard explained Salesforce eSignatures: Generate, sign, and track documents in one flow Online document viewer: Options, tradeoffs, and how to embed one Document viewer for web apps: React, Vue, Angular (2026) Best document viewers in 2026: A buyer’s guide How to edit a PDF in Python: Add text, images, and annotations Nutrient advances Workflow platform with agentic AI for enterprise-grade speed and consistency in document-heavy operations How to create a Salesforce quote template from opportunity data The business case for accessibility: Five ways it drives enterprise value Python PDF library comparison (2026): 7 libraries for developers Why your AI agent hallucinates PDF table data PDF.js limitations: When to upgrade to a commercial PDF SDK
How to set up a custom PDF.js viewer in React
Austin Nguyen · 2026-06-03 · via Inside Nutrient

Table of contents

    This guide walks through setting up a complete PDF viewer from scratch using React and pdfjs-dist, giving you full control over rendering, events, and layering for annotations, highlights, and custom toolbars.

    How to set up a custom PDF.js viewer in React

    TL;DR

    Build a custom PDF.js viewer in React directly on pdfjs-dist, without a wrapper library. The setup has nine pieces:

    • Configure GlobalWorkerOptions.workerSrc before loading any document.
    • Set cMapUrl and standardFontDataUrl for Chinese, Japanese, and Korean (CJK) and standard font rendering.
    • Wire four core components: EventBus, PDFLinkService, PDFFindController, PDFViewer.
    • Connect the loaded document to all three (setDocument).
    • Set initial scale inside the pagesinit event.
    • Store PDF.js objects in useRef (mutable, external) and expose via React Context.
    • Import pdfjs-dist/web/pdf_viewer.css or the text and annotation layers won’t render.
    • Destroy the loading task and document on unmount.
    • Call viewer.update() to force a refresh after panel resizes.

    If you’d rather not wire this up, Nutrient Web SDK does the same with NutrientViewer.load({ container, document }).

    PDF.js ships a full viewer layer (pdfjs-dist/web/pdf_viewer.mjs) that most tutorials ignore in favor of wrapper libraries like react-pdf. Building directly on PDF.js gives you full control over rendering, events, and layering, which you’ll need for annotations, highlights, and custom toolbars.

    Install

    Step 1: Configure the web worker

    PDF.js offloads parsing to a web worker. You must point GlobalWorkerOptions.workerSrc to the worker file before loading any document:

    import { GlobalWorkerOptions, version } from "pdfjs-dist";

    GlobalWorkerOptions.workerSrc = new URL(

    "pdfjs-dist/legacy/build/pdf.worker.min.mjs",

    import.meta.url,

    ).toString();

    Why legacy/build? The legacy build includes polyfills for broader browser support. If you only target modern browsers, use build/pdf.worker.min.mjs instead.

    Step 2: Set default options

    PDF.js needs CMap files for CJK fonts and standard font data for proper rendering. You can serve these from unpkg or bundle them yourself:

    const pdfDefaultOptions = {

    cMapUrl: `https://unpkg.com/pdfjs-dist@${version}/cmaps/`,

    standardFontDataUrl: `https://unpkg.com/pdfjs-dist@${version}/standard_fonts`,

    rangeChunkSize: 1024 * 1024, // 1MB chunks for range requests

    };

    Step 3: Initialize the core components

    PDF.js’s viewer layer has four key components that wire together:

    const pdfjs = await import("pdfjs-dist/web/pdf_viewer.mjs");

    // 1. `EventBus` — the central pub/sub for all PDF.js events.

    const eventBus = new pdfjs.EventBus();

    // 2. `LinkService` — handles internal/external links in the PDF.

    const linkService = new pdfjs.PDFLinkService({

    eventBus,

    externalLinkTarget: pdfjs.LinkTarget.BLANK,

    });

    // 3. `FindController` — runs text search.

    const findController = new pdfjs.PDFFindController({

    linkService,

    eventBus,

    });

    // 4. `PDFViewer` — the actual rendering engine.

    const viewer = new pdfjs.PDFViewer({

    container: document.getElementById("pdf-container"),

    eventBus,

    linkService,

    findController,

    });

    // Wire the link service back to the viewer.

    linkService.setViewer(viewer);

    Step 4: Load a document

    Create a loading task with getDocument and await the document. Then connect it to the link service, find controller, and viewer:

    import { getDocument } from "pdfjs-dist";

    const loadingTask = getDocument({

    ...pdfDefaultOptions,

    url: "https://example.com/document.pdf",

    });

    const pdfDocument = await loadingTask.promise;

    // Connect document to all components.

    linkService.setDocument(pdfDocument);

    findController.setDocument(pdfDocument);

    viewer.setDocument(pdfDocument);

    Step 5: Set initial scale on page init

    PDF.js fires a pagesinit event once all pages are laid out. Set the initial scale here:

    eventBus.on("pagesinit", () => {

    viewer.currentScaleValue = "auto"; // or "page-width", "page-fit", "1.5"

    });

    Step 6: Wrap it in React with Context

    The key pattern is to store all PDF.js objects in refs (they’re mutable, external instances) and expose them via React Context:

    import React from "react";

    interface PDFContextProps {

    viewer: React.RefObject<PDFViewer | undefined>;

    eventBus: React.RefObject<EventBus | undefined>;

    pdfDocument: React.RefObject<PDFDocumentProxy | undefined>;

    linkService: React.RefObject<PDFLinkService | undefined>;

    findController: React.RefObject<PDFFindController | undefined>;

    setCurrentPageNumber: (page: number) => void;

    setCurrentScaleValue: (scale: string) => void;

    setPagesRotation: (delta: number) => void;

    }

    export const PDFContext = React.createContext<PDFContextProps | undefined>(undefined);

    Use useRef for the mutable PDF.js objects (they don’t trigger rerenders), and provide setter functions for actions that should update the viewer:

    function PDFLoader({ children }: { children: React.ReactNode }) {

    const viewer = React.useRef<PDFViewer>();

    const eventBus = React.useRef<EventBus>();

    const pdfDocument = React.useRef<PDFDocumentProxy>();

    const linkService = React.useRef<PDFLinkService>();

    const findController = React.useRef<PDFFindController>();

    // ...initialize the refs in a `useEffect` using the code from steps 3–5.

    const contextValue = {

    viewer,

    eventBus,

    pdfDocument,

    linkService,

    findController,

    setCurrentPageNumber: (page: number) => {

    if (viewer.current) viewer.current.currentPageNumber = page;

    },

    setCurrentScaleValue: (scale: string) => {

    if (viewer.current) viewer.current.currentScaleValue = scale;

    },

    setPagesRotation: (delta: number) => {

    if (viewer.current) viewer.current.pagesRotation += delta;

    },

    };

    return <PDFContext.Provider value={contextValue}>{children}</PDFContext.Provider>;

    }

    Step 7: The container HTML

    Render a container element for PDF.js to mount into, with a child .pdfViewer element for the pages:

    import "pdfjs-dist/web/pdf_viewer.css";

    function PDFViewerContainer() {

    return (

    <div

    id="pdf-container"

    style={{ position: "absolute", width: "100%", height: "100%" }}

    >

    <div id="viewer" className="pdfViewer" />

    </div>

    );

    }

    You must import pdfjs-dist/web/pdf_viewer.css for the text layer, annotation layer, and page layout to work correctly.

    Step 8: Cleanup on unmount

    Always destroy the loading task and worker when the component unmounts:

    React.useEffect(() => {

    return () => {

    if (loadingTask.current && !loadingTask.current.destroyed) {

    loadingTask.current.destroy();

    }

    pdfDocument.current?.destroy();

    };

    }, []);

    loadingTask.destroy() terminates the underlying worker — no need to reach into the private _worker field.

    Step 9: Force refresh on resize

    If your viewer lives in a resizable panel, the rendered pages may not reflow. Use a custom event to force a rerender:

    // Register listener.

    window.addEventListener("refresh-pdf-viewer", () => {

    viewer.update(); // forces a layout pass.

    });

    // Dispatch from anywhere (e.g. sidebar toggle).

    window.dispatchEvent(new CustomEvent("refresh-pdf-viewer"));

    Complete architecture

    PDFLoader (Context Provider)

    |-- EventBus (pub/sub)

    |-- PDFLinkService (links + navigation)

    |-- PDFFindController (text search)

    |-- PDFViewer (rendering)

    |

    |-- PDFViewerContainer (DOM container)

    |-- SearchToolbar (dispatches find events)

    |-- PageToolbar (page navigation)

    |-- AnnotationSystem (custom overlays)

    Key takeaways

    • Use pdfjs-dist directly instead of wrapper libraries when you need custom annotations, highlights, or toolbars.
    • All PDF.js components communicate through EventBus — learn the event names.
    • Store PDF.js objects in useRef, not useState — they’re mutable external instances.
    • Always configure the worker, CMap URLs, and standard font URLs before loading.
    • Import pdfjs-dist/web/pdf_viewer.css or your text/annotation layers won’t render.

    FAQ

    Wrapper libraries are faster to get running but hide the PDFViewer, EventBus, and PDFLinkService APIs you need for custom annotations, highlight overlays, text-layer manipulation, or a custom toolbar. Building on pdfjs-dist directly gives you full access to the viewer layer at the cost of more setup code.

    legacy/build ships transpiled output with polyfills for older browsers. build is the modern ES module build with no polyfills. Use legacy/build if you need to support Safari < 15.4, Chrome < 95, or any browser that doesn’t support top-level await and modern syntax. Use build for evergreen browsers only.

    PDF.js’s PDFViewer renders three layers per page: the canvas (image), the text layer (selection), and the annotation layer (links and form fields). The CSS positions and stacks these layers correctly. Without it, pages render but text selection breaks and annotations appear in the wrong place.

    PDFViewer, EventBus, and friends are mutable, external instances — assigning a new value doesn’t change their identity, and storing them in useState would trigger unnecessary rerenders and stale closures. useRef keeps a stable reference across renders without coupling to React’s render cycle.

    Call loadingTask.destroy() to terminate the worker and abort any in-flight parsing. Then use pdfDocument.destroy() to free the document. Wrap both in a useEffect cleanup function. The worker is shut down by loadingTask.destroy() — you don’t need to reach into the private _worker field.

    Nutrient replaces the entire nine-step setup with one function call: NutrientViewer.load({ container, document }). No worker configuration, no manual EventBus/LinkService/FindController wiring, no CSS imports, no cleanup code. See the migration guide for switching from PDF.js.

    How Nutrient Web SDK handles this

    The entire PDF.js setup above — worker configuration, EventBus, LinkService, FindController, PDFViewer wiring, cleanup — is replaced by a single function call:

    import NutrientViewer from "@nutrient-sdk/viewer";

    NutrientViewer.load({

    container: "#pdf-container",

    document: "document.pdf",

    });

    Nutrient’s WebAssembly-based rendering engine handles text selection, search, annotations, and forms natively — with none of the wiring above.

    See Nutrient Web SDK for an alternative to PDF.js, or follow the migration guide to switch. Talk to Sales about your requirements.

    Explore related topics

    Try for free Ready to get started?

    Related SDK articles

    Explore more