Build a React PDF viewer with pdfjs-dist and Next.js: Step-by-step tutorial

Table of contents

    Need to add PDF viewing to your React or Next.js app? This tutorial walks you through creating a custom PDF viewer using pdfjs-dist, the official npm package for Mozilla’s open source PDF.js library, and compares it to Nutrient’s commercial SDK — perfect if you need annotations, editing, or real-time collaboration.
    Build a React PDF viewer with pdfjs-dist and Next.js: Step-by-step tutorial
    Summary

    Learn how to build a PDF viewer in React using pdfjs-dist (the official PDF.js npm package) and Next.js, and see how it compares to Nutrient’s commercial SDK.

    • pdfjs-dist (open source) — Lightweight and flexible, but requires manual setup for things like annotations, search, navigation, or multipage viewing.
    • Nutrient SDK — Provides 30+ built-in features, including annotation tools, PDF editing, form filling, and Office file support, all with minimal configuration.

    This guide walks you through both setups with working code samples, performance tips, and a side-by-side comparison to help you choose the right tool for your use case.

    In this tutorial, you’ll learn how to create a React PDF viewer using pdfjs-dist(opens in a new tab), the official npm distribution of PDF.js(opens in a new tab) — Mozilla’s open source JavaScript library for rendering PDF documents. You’ll use Next.js, a popular React framework, to demonstrate how to integrate pdfjs-dist into a React application.

    You’ll first create a PDF viewer using pdfjs-dist and then compare it to a viewer built with Nutrient Web SDK. If you need to go beyond basic rendering, Nutrient lets you:

    Introduction to PDF.js and its capabilities

    PDF.js(opens in a new tab) is an open source JavaScript library developed by Mozilla that renders PDF documents directly in web browsers. It’s distributed on npm as pdfjs-dist(opens in a new tab). PDF.js covers most common PDF viewing needs, including text selection, zooming, and page navigation.

    Why choose PDF.js for React PDF viewing?

    PDF.js works well in React applications. Installing pdfjs-dist via npm gives you the core rendering engine and web worker scripts. The web worker approach keeps rendering off the main thread. The project has an active contributor base and frequent releases.

    Integrating a PDF.js viewer with React

    PDF.js is organized into three main layers:

    • Core — Handles the parsing of the binary PDF file. This layer isn’t usually relevant to users, as the library uses it internally.
    • Display — Uses the core layer and exposes a more straightforward API to render PDFs and expose details.
    • Viewer — Built on top of the display layer, this is the UI for the PDF viewer in Firefox and the other browser extensions within the project.

    Prerequisites

    • Node.js (v18 or later) and npm installed on your machine
    • Basic familiarity with React and the command line

    Setting up a React project with pdfjs-dist

    Before rendering PDFs, set up your development environment. You’ll use Next.js, a popular React framework, to create the project structure.

    1. Run the following command to scaffold a new Next.js project inside a folder named pdfjs-demo:

      Terminal window
      npx create-next-app@latest pdfjs-demo

      When prompted, press Enter to accept the default options. This will set up the project with the recommended settings, allowing you to focus on integrating PDF.js with Next.js.

    2. Next, navigate into your project directory and install the distribution build of PDF.js, available on npm as pdfjs-dist(opens in a new tab):

      Terminal window
      cd pdfjs-demo && npm install pdfjs-dist
    3. PDF.js uses web workers to offload the parsing and rendering of PDFs to a background thread, improving performance. To keep the setup simple, manually copy the worker script to the public directory, which is served statically by Next.js. Run the following command:

      Terminal window
      cp ./node_modules/pdfjs-dist/build/pdf.worker.min.mjs ./public/

      To automate this step, you can add a script to your package.json file. This script will copy the worker file whenever you run the dev or build commands:

      "scripts": {
      "copy-assets": "cp ./node_modules/pdfjs-dist/build/pdf.worker.min.mjs ./public/",
      "dev": "next dev",
      "build": "next build",
      "dev": "npm run copy-assets && next dev",
      "build": "npm run copy-assets && next build",
      }

      This copies the worker script to the public directory on every dev or build run.

    Rendering a PDF in the application

    With the setup complete, you can now implement the functionality to render a PDF. Start by creating a React component in the src/app/page.js file. This component will load a PDF file and render its first page on a canvas.

    The following code defines a React component that uses the useEffect hook to load and render the first page of a PDF document on a canvas:

    app.js
    "use client";
    68 collapsed lines
    import { useEffect, useRef } from "react";
    export default function App() {
    const canvasRef = useRef(null);
    const renderTaskRef = useRef(null); // Ref to store the current render task.
    useEffect(() => {
    let isCancelled = false;
    (async function () {
    // Import pdfjs-dist dynamically for client-side rendering.
    const pdfJS = await import("pdfjs-dist");
    // Set up the worker.
    pdfJS.GlobalWorkerOptions.workerSrc =
    window.location.origin + "/pdf.worker.min.mjs";
    // Load the PDF document.
    const pdf = await pdfJS.getDocument("example.pdf").promise;
    // Get the first page.
    const page = await pdf.getPage(1);
    const viewport = page.getViewport({ scale: 1.5 });
    // Prepare the canvas.
    const canvas = canvasRef.current;
    const canvasContext = canvas.getContext("2d");
    canvas.height = viewport.height;
    canvas.width = viewport.width;
    // Ensure no other render tasks are running.
    if (renderTaskRef.current) {
    await renderTaskRef.current.promise;
    }
    // Render the page into the canvas.
    const renderContext = { canvasContext, viewport };
    const renderTask = page.render(renderContext);
    // Store the render task.
    renderTaskRef.current = renderTask;
    // Wait for rendering to finish.
    try {
    await renderTask.promise;
    } catch (error) {
    if (error.name === "RenderingCancelledException") {
    console.log("Rendering cancelled.");
    } else {
    console.error("Render error:", error);
    }
    }
    if (!isCancelled) {
    console.log("Rendering completed");
    }
    })();
    // Cleanup function to cancel the render task if the component unmounts.
    return () => {
    isCancelled = true;
    if (renderTaskRef.current) {
    renderTaskRef.current.cancel();
    }
    };
    }, []);
    return <canvas ref={canvasRef} style={{ height: "100vh" }} />;
    }

    The example.pdf file in the code above is present in the public directory. The useEffect hook with no dependencies gets executed once, just after the component is first rendered.

    Note that the component tracks the current render task using a ref (renderTaskRef) and ensures that multiple render operations don’t overlap. If a new render task starts while the previous one is still running, the new task waits for the previous one to complete.

    You can now run the application using npm run dev, and the application will open on the localhost:3000 port.

    As you can see, you can only display the first page of your document by default. If you need any other functionality — like page navigation, search, or annotations — you’ll have to implement it yourself.

    Handling common issues with PDF.js in React

    When integrating PDF.js into a React application, developers may face several challenges. Here are common issues and how to fix them.

    Rendering issues

    Problem

    PDF.js relies on a <canvas> element to render PDF pages. Issues often arise with canvas size, scaling, and responsiveness within React components.

    Solutions

    • Proper sizing — Ensure the canvas element is correctly sized to fit its container. Use CSS or inline styles to set the dimensions, and consider using React’s useEffect to adjust the size based on the window or container size.

    • Scaling — PDF.js provides scaling options to render PDFs at different zoom levels. Configure the scale factor properly to ensure the content is rendered clearly and fits within the viewable area.

    • Viewport management — Use PDF.js’s viewport settings to manage how pages are displayed. Adjust the viewport based on the container size or user interactions to maintain a responsive design.

      Example code:

    useEffect(() => {
    const renderPage = (pageNumber) => {
    pdf.getPage(pageNumber).then((page) => {
    const viewport = page.getViewport({ scale: 1.5 });
    const canvas = canvasRef.current;
    const context = canvas.getContext("2d");
    canvas.height = viewport.height;
    canvas.width = viewport.width;
    const renderContext = {
    canvasContext: context,
    viewport: viewport,
    };
    page.render(renderContext);
    });
    };
    renderPage(1);
    }, []);

    Worker setup

    Problem

    PDF.js uses web workers to handle PDF rendering tasks off the main thread. Incorrect setup or configuration of these workers can lead to performance issues or failures in rendering.

    Solutions

    • Worker path — Ensure the path to the PDF.js worker file is correctly specified. The worker script must be accessible from the client-side.
    • Web worker configuration — Configure PDF.js to use web workers by setting the workerSrc property correctly. This can be done in your React component or globally in your application setup.

    Example code:

    import * as pdfjsLib from "pdfjs-dist";
    pdfjsLib.GlobalWorkerOptions.workerSrc = "/path-to/pdf.worker.min.mjs";

    Cross-browser compatibility

    Problem

    Different browsers may have varying levels of support for HTML5 features used by PDF.js, leading to inconsistencies in rendering or functionality.

    Solutions

    • Testing — Regularly test your application on multiple browsers (Chrome, Firefox, Safari, Edge) to ensure consistent behavior and appearance.
    • Polyfills — Use polyfills for missing or inconsistent features in certain browsers. This can help ensure compatibility across different environments.
    • Feature detection — Implement feature detection to provide fallback solutions or graceful degradation if certain features aren’t supported.

    Example code:

    if (!("HTMLCanvasElement" in window)) {
    console.error("Canvas not supported");
    // Provide fallback or notification.
    }

    Troubleshooting pdfjs-dist in Next.js

    If you run into issues integrating pdfjs-dist with Next.js, here are the most common problems and how to fix them.

    Worker file not found (404)

    Problem — The browser console shows a 404 error for pdf.worker.min.mjs.

    Fix — Make sure you’ve copied the worker file to your public/ directory. The path in workerSrc must match the actual file location:

    // Correct — file must exist at public/pdf.worker.min.mjs.
    pdfJS.GlobalWorkerOptions.workerSrc = "/pdf.worker.min.mjs";

    If you upgraded pdfjs-dist, the worker file name may have changed. Re-run the copy command:

    Terminal window
    cp ./node_modules/pdfjs-dist/build/pdf.worker.min.mjs ./public/

    Missing “use client” directive

    Problem — Next.js throws a server-side rendering error because pdfjs-dist accesses browser APIs (window, document, canvas) that don’t exist on the server.

    Fix — Add "use client" at the top of any component that imports or uses pdfjs-dist:

    "use client";
    import { useEffect, useRef } from "react";
    // Now safe to use pdfjs-dist here.

    Version mismatch between pdfjs-dist and its worker

    Problem — The viewer loads but pages render as blank, or the console shows Setting up fake worker warnings.

    Fix — The pdfjs-dist package version and the worker script version must match exactly. After upgrading, always recopy the worker file and confirm the versions align:

    Terminal window
    npm ls pdfjs-dist
    cp ./node_modules/pdfjs-dist/build/pdf.worker.min.mjs ./public/

    Canvas hydration mismatch

    Problem — Next.js warns about a hydration mismatch because the server-rendered HTML doesn’t include canvas content.

    Fix — Wrap your PDF rendering in a dynamic import with SSR disabled, or use conditional rendering:

    import dynamic from "next/dynamic";
    const PDFViewer = dynamic(() => import("./PDFViewer"), {
    ssr: false,
    });

    These issues are specific to using raw pdfjs-dist. With Nutrient Web SDK, worker configuration, SSR handling, and asset management are handled automatically — so you can focus on building your application instead of debugging integration issues.

    Setting up Nutrient Web SDK with Next.js

    If the pdfjs-dist setup above feels like a lot of work for just displaying a single page, Nutrient’s React PDF viewer gives you a production-ready viewer — with annotations, form filling, signatures, Office file support, and real-time collaboration — without building any of it yourself.

    Follow the steps below to set it up in the same amount of time.

    1. Start a new project using create-next-app:

      Terminal window
      npx create-next-app@latest nutrient-demo
      cd nutrient-demo
    2. In app/layout.js, load Nutrient Web SDK from the CDN using Next.js’s <Script> component. No extra dependencies or webpack configuration are needed:

      import Script from "next/script";
      export default function RootLayout({ children }) {
      return (
      <html lang="en">
      <body>
      <Script
      src="https://cdn.cloud.pspdfkit.com/pspdfkit-web/latest/nutrient-viewer.js"
      strategy="beforeInteractive"
      />
      {children}
      </body>
      </html>
      );
      }
    3. Render a PDF in app/page.js:

      "use client";
      import { useEffect, useRef } from "react";
      export default function Home() {
      const containerRef = useRef(null);
      useEffect(() => {
      const container = containerRef.current;
      if (container && window.NutrientViewer) {
      window.NutrientViewer.load({
      container,
      document: "example.pdf", // Place this file in the public/ directory.
      });
      }
      return () => {
      if (window.NutrientViewer && container) {
      window.NutrientViewer.unload(container);
      }
      };
      }, []);
      return (
      <div ref={containerRef} style={{ height: "100vh", width: "100%" }} />
      );
      }
    4. Start the development server:

      Terminal window
      npm run dev

    Visit http://localhost:3000, where you’ll see the PDF rendered using Nutrient Web SDK.

    That’s it — no webpack plugins, no asset copying, no extra build scripts. The CDN handles loading the SDK and its assets automatically.

    Performance and security best practices for React PDF viewers

    Follow these practices when building a React PDF viewer:

    • Use a custom PDF worker URL — Offload heavy parsing and rendering tasks to a separate thread to keep the UI responsive.
    • Serve over HTTPS — Always deliver your app and PDF files over HTTPS to protect against tampering and interception.
    • Apply a Content Security Policy (CSP) — Limit allowed sources for scripts, workers, and fonts to mitigate XSS attacks.
    • Enable a Web Application Firewall (WAF) — Add an extra security layer to detect and block common exploits at the network level.

    Nutrient adds encryption, CSP support, and secure deployment guidance for production use.

    pdfjs-dist vs. Nutrient: Side-by-side comparison

    The table below compares pdfjs-dist and Nutrient by what you can deliver to your users, not just what each library includes.

    What you need to deliverpdfjs-distNutrient SDK
    Display a PDF in the browserManual canvas setup, worker configuration, and render-task managementOne-line NutrientViewer.load() call with built-in UI
    Let users annotate documentsBuild a custom toolbar and annotation engine from scratch15+ annotation tools ready to use
    Fill and submit PDF formsNot supported — you’d need a separate libraryBuilt-in form filler with validation
    Collect signaturesNot supportedDigital and electronic signatures
    Open Office files (DOCX, XLSX, PPTX)Not supported — requires a server-side conversion stepClient-side Office viewing in the same viewer
    Enable real-time collaborationBuild your own sync layer with WebSockets or CRDTBuilt-in real-time sync
    Search text inside a PDFImplement text layer extraction and search UI manuallyBuilt-in search with highlighting
    Go to production with confidenceYou maintain rendering, workers, and cross-browser fixesDedicated support team and enterprise-grade security

    When to use pdfjs-dist vs. Nutrient

    Choose pdfjs-dist if:

    • You only need to display PDFs — no annotations, forms, or signatures.
    • You want full control over the rendering pipeline and UI.
    • Your team has the bandwidth to build and maintain custom viewer features.
    • You’re building a lightweight prototype or internal tool.

    Choose Nutrient if:

    • You need annotations, form filling, or digital signatures and don’t want to build them yourself.
    • You need to open Office files or images in the same viewer as PDFs.
    • You’re on a deadline and want a production-ready viewer out of the box.
    • You need enterprise support, SLAs, or compliance guarantees.
    • You want real-time collaboration without building a sync layer.

    Conclusion

    In this tutorial, you built a React PDF viewer two ways: first with pdfjs-dist, where you configured workers, managed canvas rendering, and handled cleanup manually; then with Nutrient Web SDK, where a single load() call gave you a complete viewer with annotations, forms, and signatures included.

    pdfjs-dist is a solid choice when you only need to display PDFs and want full control over every detail. But if your users need to annotate, fill forms, sign documents, or collaborate — and you’d rather ship those features this week instead of building them over the next quarter — Nutrient’s PDF SDK handles all of that out of the box.

    Try it for free, or see the live demo to explore what’s included.

    We created similar how-to blog posts using different web frameworks and libraries:

    FAQ

    What is pdfjs-dist, and how does it relate to PDF.js?

    pdfjs-dist is the official npm package for PDF.js, Mozilla’s open source PDF rendering library. It includes the prebuilt display and worker layers, so you can install it with npm install pdfjs-dist and start rendering PDFs in your React, Next.js, or vanilla JavaScript project without building PDF.js from source.

    Why do I need to use a worker script with PDF.js?

    PDF.js uses web workers to handle the heavy lifting of PDF parsing and rendering in a separate thread. This prevents the main thread from being blocked, keeping the UI responsive during rendering.

    Can I customize the appearance and functionality of the PDF viewer?

    Yes. You can customize the PDF viewer by styling the components with CSS and adding features like text search, annotations, and custom navigation controls using the PDF.js API.

    What are some common challenges when building a PDF viewer with React.js and PDF.js?

    Common challenges include managing state and performance, handling large or complex PDF documents, ensuring cross-browser compatibility, and implementing advanced features like text extraction and annotations.

    What is the Nutrient SDK, and how is it different from PDF.js?

    The Nutrient SDK is a commercial PDF library designed for developers who need advanced features like annotations, form filling, digital signatures, collaboration, and support for Office and image files. Unlike PDF.js, which requires manual implementation for most features, Nutrient offers more than 30 out-of-the-box tools with minimal configuration.

    How do I use the Nutrient SDK in a React or Next.js app?

    You can integrate the Nutrient SDK by installing the @nutrient-sdk/viewer package and including its script in your layout using Next.js’s <Script> component. Then, use the NutrientViewer.load() method to render a PDF or DOCX inside a React component. We provide full instructions and copy-ready code samples in the tutorial.

    What features does the Nutrient SDK support that PDF.js doesn’t?

    The Nutrient SDK includes built-in support for annotations, PDF editing, form creation and filling, electronic and digital signatures, PDF/A generation, document redaction, real-time collaboration, and Office document viewing — features that require significant custom development in PDF.js.

    Is Nutrient secure and compliant for enterprise use?

    Yes. Nutrient supports encryption, CSP headers, secure deployment practices, and common compliance standards. Detailed guidance is available in our security documentation(opens in a new tab).

    How do I fix pdfjs-dist worker errors in Next.js?

    The most common cause is a missing or mismatched worker file. Make sure you copy the worker script from node_modules/pdfjs-dist/build/pdf.worker.min.mjs to your public/ directory, and that the version matches your installed pdfjs-dist package. Also ensure your component includes "use client" at the top, since pdfjs-dist requires browser APIs that aren’t available during server-side rendering.

    What is the difference between pdfjs-dist and react-pdf?

    pdfjs-dist is the raw PDF.js library from Mozilla — it gives you low-level control over PDF rendering via canvas but requires you to build the UI yourself. react-pdf (by wojtekmaj) is a React wrapper around pdfjs-dist that provides <Document> and <Page> components for easier integration. Both are open source and limited to rendering; neither include annotations, form filling, or signatures.

    Can I try the Nutrient SDK for free?

    Yes. Nutrient offers a free trial when you sign up. You can evaluate all major features — including viewing, annotation, form filling, and PDF export — directly in your local or production environment.

    Ritesh Kumar

    Ritesh Kumar

    Web Senior Staff Software Engineer

    Ritesh loves to write code, play keyboard, and paint. He likes working on projects that involve developer tooling, design systems, and music. He wants to make art a part of everyone’s life by using technology.

    Hulya Masharipov

    Hulya Masharipov

    Technical Writer

    Hulya is a frontend web developer and technical writer who enjoys creating responsive, scalable, and maintainable web experiences. She’s passionate about open source, web accessibility, cybersecurity privacy, and blockchain.

    Explore related topics

    Try for free Ready to get started?