Build a React PDF viewer with PDF.js and Next.js: Step-by-step tutorial
Table of contents

Learn how to build a PDF viewer in React using PDF.js and Next.js, and see how it compares to Nutrient’s commercial SDK.
- PDF.js (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, like annotation tools, PDF editing, form filling, and Office file support, 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.js viewer using PDF.js(opens in a new tab), a powerful JavaScript library for rendering PDF documents. You’ll use Next.js, a popular React framework, to demonstrate how to integrate PDF.js into a React application.
You’ll first create a PDF viewer using PDF.js and then compare it to a viewer built with Nutrient Web SDK. Our viewer library provides some benefits beyond what PDF.js delivers, including:
- A prebuilt UI — Save time with a well-documented list of APIs when customizing the UI to meet your exact requirements.
- Annotation tools — Draw, circle, highlight, comment, and add notes to documents with 15+ prebuilt annotation tools.
- Multiple file types — Support client-side viewing of PDFs, MS Office documents, and image files.
- 30+ features — Easily add features like PDF editing, digital signatures, form filling, real-time document collaboration, and more.
- Dedicated support — Deploy faster by working 1-on-1 with our developers.
Introduction to PDF.js and its capabilities
PDF.js(opens in a new tab) is an open source JavaScript library developed by Mozilla that enables the rendering of PDF documents directly in web browsers. Initially created to offer a consistent and high-quality PDF viewing experience across all modern browsers, PDF.js has grown into a robust solution for various PDF viewing needs. It supports a wide range of PDF features, including text selection, zooming, and navigation, making it an excellent choice for integrating PDF functionality into web applications.
Why choose PDF.js for React PDF viewing?
PDF.js is particularly well-suited for React applications due to its performance, ease of integration, and strong community support. Its architecture allows seamless integration with React components, and its efficient rendering capabilities ensure smooth performance, even with large documents. The active community around PDF.js also means ongoing improvements and support, making it a reliable choice for React developers.
Integrating a PDF.js viewer with React
PDF.js is organized into three main layers:
- Core — This layer handles the parsing of the binary PDF file. This layer isn’t usually relevant to the user, as the library uses this internally.
- Display — This layer uses the core layer and exposes a more straightforward API to render PDFs and expose details.
- Viewer — The viewer is built on top of the display layer and is the UI for the PDF viewer in Firefox and the other browser extensions within the project.
Setting up a React project with PDF.js
To get started with rendering PDFs using PDF.js in a React application, you first need to set up our development environment. You’ll use Next.js, a popular React framework, to create the project structure.
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-demoWhen 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.
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-distPDF.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 setup ensures that the worker script is always copied to the public directory before the application starts, avoiding manual steps and potential errors.
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:
"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/build/pdf");
// 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’s a deeper look at common issues and how to address 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 that 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 that 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.}
Setting up Nutrient Web SDK with Next.js
Nutrient offers a versatile React PDF viewer library that provides more than 30 out-of-the-box features, including:
- 15+ PDF annotation tools
- PDF form filling and creation
- Digital and electronic signatures
- Document editing (rotate, merge, split, and more)
- Image and Office file viewing
- Real-time collaboration
- And much more
Start a new project using
create-next-app
:Terminal window npx create-next-app@latest nutrient-democd nutrient-demoInstall Nutrient Web SDK and the webpack copy plugin:
Terminal window npm install @nutrient-sdk/viewer copy-webpack-pluginCreate or update
next.config.mjs
(use CommonJS syntax):import CopyPlugin from "copy-webpack-plugin";import path from "node:path";const nextConfig = {webpack: (config, { isServer }) => {if (!isServer) {config.externals = config.externals || [];config.externals.push({"@nutrient-sdk/viewer": "@nutrient-sdk/viewer",});}config.plugins.push(new CopyPlugin({patterns: [{from: path.resolve(__dirname,"node_modules/@nutrient-sdk/viewer/dist",),to: path.resolve(__dirname, "public/nutrient-viewer"),force: true,noErrorOnMissing: true,},],}),);return config;},};module.exports = nextConfig;Add a
copy-assets
script topackage.json
to make sure assets are copied ondev
andbuild
:"scripts": {"copy-assets": "cp -R ./node_modules/@nutrient-sdk/viewer/dist/ public/nutrient-viewer","dev": "npm run copy-assets && next dev","build": "npm run copy-assets && next build"}Optionally add
public/nutrient-viewer/
to.gitignore
so the SDK files aren’t committed.Ensure the following structure exists:
public/nutrient-viewer/nutrient-viewer.jsnutrient-viewer-lib/In
app/layout.js
, include the Nutrient script using<Script>
:import Script from "next/script";export default function RootLayout({ children }) {return (<html lang="en"><head><Scriptsrc="/nutrient-viewer/nutrient-viewer.js"strategy="beforeInteractive"/></head><body>{children}</body></html>);}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;const { NutrientViewer } = window;if (container && NutrientViewer) {NutrientViewer.load({container,document: "example.pdf", // Replace with your file});}return () => {NutrientViewer?.unload?.(container);};}, []);return (<div ref={containerRef} style={{ height: "100vh", width: "100%" }} />);}Start the development server:
Terminal window npm run dev
Visit http://localhost:3000
— you should see the PDF rendered using Nutrient Web SDK.
You’ve now set up Nutrient Web SDK with:
- Client-only rendering
- Proper script injection
- Asset management for production and development
- A clean, automated build setup
Performance and security best practices for React PDF viewers
To ensure a smooth and secure experience when building a React PDF viewer, follow these key recommendations:
- 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.
For even stronger performance and security guarantees, consider libraries like Nutrient, which emphasize enterprise-grade encryption, Content Security Policy (CSP) support, and secure deployment guidance — all designed to help protect your document workflows in production environments.
Conclusion
This tutorial looked at how to build a React PDF viewer with both the PDF.js open source library and our React PDF library that enables you to display and render PDF files in your React application.
Open source React libraries are good options for building a UI and features yourself. However, depending on the complexity of your use case, development time and costs can quickly increase. In these situations, opting for a commercial solution can speed up development time and lets you focus on other areas of your business.
At Nutrient, we offer a commercial, feature-rich, and completely customizable PDF SDK that’s easy to integrate and comes with well-documented APIs to handle advanced use cases. Try it for free, or visit our demo to see it in action.
We created similar how-to blog posts using different web frameworks and libraries:
- How to build an Angular PDF viewer with PDF.js
- How to build a Vue.js PDF viewer with PDF.js
- How to build a jQuery PDF viewer with PDF.js
- How to build a Bootstrap 5 PDF viewer with PDF.js
- How to build an Electron PDF viewer with PDF.js
- How to build a TypeScript PDF viewer with PDF.js
- How to build a JavaScript PDF viewer with PDF.js
FAQ
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, ensuring smoother performance and a better user experience.
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 is designed with enterprise-grade security in mind, including support for strong encryption, CSP header configuration, secure deployment practices, and compliance with industry standards. Detailed guidance is available in our security documentation(opens in a new tab).
Can I try the Nutrient SDK for free?
Absolutely. 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.