---
title: "How to set up a custom PDF.js viewer in React"
canonical_url: "https://www.nutrient.io/blog/pdfjs-react-viewer-setup/"
md_url: "https://www.nutrient.io/blog/pdfjs-react-viewer-setup.md"
last_updated: "2026-07-15T12:10:06.419Z"
description: "Learn how to build a complete PDF.js viewer in React from scratch using pdfjs-dist, including worker configuration, EventBus, FindController, React Context integration, and cleanup."
---

**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](https://www.nutrient.io/sdk/web-overview/) 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

```bash

npm install pdfjs-dist

```

## 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:

```tsx

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:

```ts

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:

```tsx

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:

```tsx

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:

```tsx

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:

```tsx

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:

```tsx

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:

```tsx

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:

```tsx

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:

```tsx

// 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

#### Why use <code>pdfjs-dist</code> directly instead of <code>react-pdf</code> or <code>@react-pdf-viewer/core</code>?

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.

#### What’s the difference between <code>legacy/build</code> and <code>build</code> in <code>pdfjs-dist</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.

#### Why do I need to import <code>pdf_viewer.css</code>?

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.

#### Why use <code>useRef</code> instead of <code>useState</code> for PDF.js objects?

`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.

#### How do I clean up PDF.js properly on unmount?

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.

#### How does Nutrient Web SDK compare for this setup?

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](https://www.nutrient.io/guides/web/about/migration-guides/migrating-from-mozilla-pdfjs.md) 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:

```js

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](https://www.nutrient.io/sdk/web-overview/) for an alternative to PDF.js, or follow the [migration guide](https://www.nutrient.io/guides/web/about/migration-guides/migrating-from-mozilla-pdfjs.md) to switch. [Talk to Sales](https://www.nutrient.io/contact-sales/?=sdk) about your requirements._
---

## Related pages

- [The business case for accessibility: Five ways it drives enterprise value](/blog/5-ways-accessibility-drives-enterprise-value.md)
- [Accessibility Untangled Why It Matters Guide](/blog/accessibility-untangled-why-it-matters-guide.md)
- [Advanced Techniques For React Native Ui Components](/blog/advanced-techniques-for-react-native-ui-components.md)
- [`vector_store` holds your indexed documents (see the multimodal RAG post](/blog/agentic-rag.md)
- [Ai Document Automation Extraction To Action](/blog/ai-document-automation-extraction-to-action.md)
- [Ai Legal Assistant Document Authoring](/blog/ai-legal-assistant-document-authoring.md)
- [Angular File Viewer Pdf Image Office Files](/blog/angular-file-viewer-pdf-image-office-files.md)
- [Auto Tagging And Document Accessibility In Dotnet Sdk](/blog/auto-tagging-and-document-accessibility-in-dotnet-sdk.md)
- [Best Document Viewers](/blog/best-document-viewers.md)
- [The CEO’s AI playbook: Why decision architecture beats model selection](/blog/ceo-ai-playbook-decision-architecture.md)
- [Complete Guide To Pdfjs](/blog/complete-guide-to-pdfjs.md)
- [Construction Document Data Extraction](/blog/construction-document-data-extraction.md)
- [Convert One Drive Files To Pdf In Sharepoint](/blog/convert-one-drive-files-to-pdf-in-sharepoint.md)
- [Create And Edit Pdfs In Flutter](/blog/create-and-edit-pdfs-in-flutter.md)
- [Create Pdfs With React](/blog/create-pdfs-with-react.md)
- [Creating A Document Scanner With Ocr In Python](/blog/creating-a-document-scanner-with-ocr-in-python.md)
- [Creating And Filling Pdf Forms Programmatically In Javascript](/blog/creating-and-filling-pdf-forms-programmatically-in-javascript.md)
- [The CTO’s AI playbook: Why accountability architecture beats orchestration](/blog/cto-ai-playbook-accountability-architecture.md)
- [Digital Signatures](/blog/digital-signatures.md)
- [Digital Workflow Automation](/blog/digital-workflow-automation.md)
- [Document Ai Vs Ocr](/blog/document-ai-vs-ocr.md)
- [Document Viewer](/blog/document-viewer.md)
- [Document Watermarking](/blog/document-watermarking.md)
- [Emerging threats: Your logging system may be an agentic threat vector](/blog/emerging-threats-your-logging-system.md)
- [app.py](/blog/extract-text-from-pdf-using-python.md)
- [How To Add Digital Signature To Pdf Using React](/blog/how-to-add-digital-signature-to-pdf-using-react.md)
- [How To Build A Dotnet Maui Pdf Viewer](/blog/how-to-build-a-dotnet-maui-pdf-viewer.md)
- [How To Build A Flutter Pdf Viewer](/blog/how-to-build-a-flutter-pdf-viewer.md)
- [or](/blog/how-to-build-a-javascript-pdf-viewer-with-pdfjs.md)
- [How To Build A Javascript Pdf Viewer](/blog/how-to-build-a-javascript-pdf-viewer.md)
- [or](/blog/how-to-build-a-nextjs-pdf-viewer.md)
- [How To Build A Powerpoint Viewer Using Javascript](/blog/how-to-build-a-powerpoint-viewer-using-javascript.md)
- [Using Yarn](/blog/how-to-build-a-react-excel-viewer.md)
- [How To Build A React Native Pdf Viewer](/blog/how-to-build-a-react-native-pdf-viewer.md)
- [How To Build A React Powerpoint Viewer](/blog/how-to-build-a-react-powerpoint-viewer.md)
- [How To Build A Reactjs File Viewer](/blog/how-to-build-a-reactjs-file-viewer.md)
- [or](/blog/how-to-build-a-reactjs-pdf-viewer-with-react-pdf.md)
- [or](/blog/how-to-build-a-reactjs-pdf-viewer.md)
- [How To Build A Reactjs Viewer With Pdfjs](/blog/how-to-build-a-reactjs-viewer-with-pdfjs.md)
- [How To Build A Vuejs Pdf Viewer With Pdfjs](/blog/how-to-build-a-vuejs-pdf-viewer-with-pdfjs.md)
- [How To Build A Vuejs Pdf Viewer](/blog/how-to-build-a-vuejs-pdf-viewer.md)
- [How To Build An Android Pdf Viewer](/blog/how-to-build-an-android-pdf-viewer.md)
- [How To Build An Angular Pdf Viewer With Ng2 Pdf Viewer](/blog/how-to-build-an-angular-pdf-viewer-with-ng2-pdf-viewer.md)
- [How To Build An Angular Pdf Viewer With Pdfjs](/blog/how-to-build-an-angular-pdf-viewer-with-pdfjs.md)
- [How To Convert Docx To Pdf Using Javascript](/blog/how-to-convert-docx-to-pdf-using-javascript.md)
- [How To Convert Docx To Pdf Using Python](/blog/how-to-convert-docx-to-pdf-using-python.md)
- [How To Convert Html To Pdf Using Html2pdf](/blog/how-to-convert-html-to-pdf-using-html2pdf.md)
- [or](/blog/how-to-convert-html-to-pdf-using-react.md)
- [or](/blog/how-to-convert-html-to-pdf-using-wkhtmltopdf-and-python.md)
- [How To Convert Word To Pdf In Nodejs](/blog/how-to-convert-word-to-pdf-in-nodejs.md)
- [or](/blog/how-to-create-a-react-js-signature-pad.md)
- [How To Create Pdfs With React To Pdf](/blog/how-to-create-pdfs-with-react-to-pdf.md)
- [How To Edit Pdfs Using Ios Pdf Library](/blog/how-to-edit-pdfs-using-ios-pdf-library.md)
- [How To Embed A Pdf Viewer In Your Website](/blog/how-to-embed-a-pdf-viewer-in-your-website.md)
- [How To Generate Pdf From Html With Nodejs](/blog/how-to-generate-pdf-from-html-with-nodejs.md)
- [base_url tells WeasyPrint where to resolve relative asset paths](/blog/how-to-generate-pdf-reports-from-html-in-python.md)
- [Open an image.](/blog/how-to-use-tesseract-ocr-in-python.md)
- [From an HTML string.](/blog/html-in-pdf-format.md)
- [Javascript Pdf Libraries](/blog/javascript-pdf-libraries.md)
- [Linearized Pdf](/blog/linearized-pdf.md)
- [Swift Package Manager](/blog/mobile-pdf-sdk.md)
- [`elements` come from your document parser — each has a type and content.](/blog/multimodal-rag.md)
- [Nutrient Vs Conga Composer](/blog/nutrient-vs-conga-composer.md)
- [Online Document Viewer](/blog/online-document-viewer.md)
- [Open Pdf In Your Web App](/blog/open-pdf-in-your-web-app.md)
- [Pdf Page Labels](/blog/pdf-page-labels.md)
- [Pdf Sdk Compliance Security Checklist](/blog/pdf-sdk-compliance-security-checklist.md)
- [Pdf Sdk Performance Benchmark](/blog/pdf-sdk-performance-benchmark.md)
- [Pdf Ua Compliance Guide](/blog/pdf-ua-compliance-guide.md)
- [Pdfjs Annotation Editor Layer](/blog/pdfjs-annotation-editor-layer.md)
- [Pdfjs Area Annotations Canvas Capture](/blog/pdfjs-area-annotations-canvas-capture.md)
- [Pdfjs Coordinate Systems Pdf To Screen](/blog/pdfjs-coordinate-systems-pdf-to-screen.md)
- [Pdfjs Eventbus Guide](/blog/pdfjs-eventbus-guide.md)
- [Pdfjs Limitations Commercial Upgrade](/blog/pdfjs-limitations-commercial-upgrade.md)
- [Pdfjs Native Annotation Layer Forms](/blog/pdfjs-native-annotation-layer-forms.md)
- [Pdfjs Navigation Zoom Rotation](/blog/pdfjs-navigation-zoom-rotation.md)
- [Pdfjs Pdf Page Manipulation Pdf Lib](/blog/pdfjs-pdf-page-manipulation-pdf-lib.md)
- [Pdfjs Rendering Overlays React Portals](/blog/pdfjs-rendering-overlays-react-portals.md)
- [Pdfjs Server Side Text Extraction](/blog/pdfjs-server-side-text-extraction.md)
- [Pdfjs Sticky Note Annotations](/blog/pdfjs-sticky-note-annotations.md)
- [Pdfjs Text Highlight Annotations](/blog/pdfjs-text-highlight-annotations.md)
- [Pdfjs Text Search Pdffindcontroller](/blog/pdfjs-text-search-pdffindcontroller.md)
- [Pdfjs Thumbnail Sidebar](/blog/pdfjs-thumbnail-sidebar.md)
- [Process Flows](/blog/process-flows.md)
- [React Native Pdf Annotation](/blog/react-native-pdf-annotation.md)
- [or](/blog/sample-blog-updated.md)
- [Open an image file.](/blog/tesseract-python-guide.md)
- [Top 5 Javascript Pdf Viewers](/blog/top-5-javascript-pdf-viewers.md)
- [Convert an HTML file to PDF.](/blog/top-ten-ways-to-convert-html-to-pdf.md)
- [Vector Pdf](/blog/vector-pdf.md)
- [Wcag2 Accessibility Requirements Documents](/blog/wcag2-accessibility-requirements-documents.md)
- [Web Sdk Is Now Headless](/blog/web-sdk-is-now-headless.md)
- [What Are Annotations](/blog/what-are-annotations.md)
- [What Is A Vpat](/blog/what-is-a-vpat.md)
- [What Is Pdf Ua](/blog/what-is-pdf-ua.md)
- [Why Pdfium Is A Trusted Platform For Pdf Rendering](/blog/why-pdfium-is-a-trusted-platform-for-pdf-rendering.md)
- [Why Your Ai Agent Hallucinates Pdf Table Data](/blog/why-your-ai-agent-hallucinates-pdf-table-data.md)

