How to build a React PDF viewer with PDF.js and Next.js
This article was first published in November 2021 and was updated in November 2024.
In this tutorial, you’ll learn how to create a React PDF.js viewer using PDF.js, 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 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 Next.js application
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.
-
Creating a new Next.js app
Run the following command to scaffold a new Next.js project inside a folder named pdfjs-demo:
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.
-
Installing PDF.js
Next, navigate into your project directory and install the distribution build of PDF.js, available on npm as pdfjs-dist
:
cd pdfjs-demo && npm install pdfjs-dist
-
Setting up PDF.js workers
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:
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'; 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. }
Optimizing Performance and Security
When building a React PDF viewer, it’s essential to optimize for performance and security. Here are some key tips:
-
Use a Custom PDF Worker URL: Improve performance by offloading PDF parsing and rendering to a web worker with a custom worker URL, preventing the main thread from being blocked.
-
Use HTTPS: Always load the application and PDF documents over HTTPS to prevent man-in-the-middle attacks and ensure data integrity.
-
Apply Content Security Policy (CSP): Set up a CSP to control content sources and prevent XSS attacks. Allow sources for worker scripts and fonts as needed.
-
Web Application Firewall (WAF): A WAF can protect your application from common attacks, adding a security layer beyond what’s possible at the component level.
Building a React PDF viewer with Nutrient
Nutrient offers a versatile React PDF viewer library that provides more than 30 out-of-the-box features, including:
-
And much more
-
Setting up the project
-
Start by creating a new Next.js project using the
create-next-app
command. This sets up a Next.js project with all the default configurations:
npx create-next-app@latest pspdfkit-demo cd pspdfkit-demo
-
Next, install the Nutrient dependency, which will be used to integrate PDF viewing and editing capabilities into your project:
npm install pspdfkit
-
Copying Nutrient assets
After installing Nutrient, you’ll need to copy its web assets to the public
directory. These assets are necessary for the SDK to function correctly in the browser.
If the public directory doesn’t exist, create it first:
mkdir public
Then, copy the assets using the following command:
cp -R ./node_modules/pspdfkit/dist/pspdfkit-lib public/pspdfkit-lib
This command copies the pspdfkit-lib
folder from the node_modules
directory into the public
directory, making it accessible at runtime. You can add this to package.json
, just like in the previous section.
-
Setting up the viewer
-
In the
app/page.jsx
file, use React’suseEffect
anduseRef
hooks to load the Nutrient viewer within adiv
element when the component mounts. This ensures that the viewer is initialized after the component has been rendered on the client side. -
The PDF viewer is configured to display a PDF document (
document.pdf
) located in thepublic
directory, making it easily accessible to the viewer. The viewer loads this document when the component is mounted, and thebaseUrl
is set dynamically based on the current window location:
// app/page.jsx 'use client'; import { useEffect, useRef, useState } from 'react'; const App = () => { const containerRef = useRef(null); const [isClient, setIsClient] = useState(false); useEffect(() => { setIsClient(true); }, []); useEffect(() => { if (isClient) { const container = containerRef.current; if (container && typeof window !== 'undefined') { import('pspdfkit').then((PSPDFKit) => { if (PSPDFKit) { PSPDFKit.unload(container); } PSPDFKit.load({ container, document: '/document.pdf', baseUrl: `${window.location.protocol}//${window.location.host}/`, }); }); } } }, [isClient]); if (!isClient) { return null; // Prevents rendering on the server. } return <div ref={containerRef} style={{ height: '100vh' }} />; }; export default App;
-
Running the application
Start the development server and view the PDF in the browser:
npm run dev
The application will open on the localhost:3000
port. All the features you expect from a PDF viewer are present by default.
Best Practices for Building a React PDF Viewer
Here are some best practices to keep in mind when building a React PDF viewer:
-
Use a Robust and Reliable PDF Library - Consider using a well-established PDF library like PDF.js or Nutrient for your project. PDF.js is widely used for rendering PDF documents, while Nutrient offers advanced features like annotations, digital signatures, and customizable UI components, making it ideal for projects with complex requirements.
-
Optimize Performance and Security - For performance, use a custom PDF worker URL to offload heavy processing. Always use secure protocols (HTTPS) to protect data integrity. Additionally, set up a Content Security Policy (CSP) and consider implementing a Web Application Firewall (WAF) to prevent vulnerabilities.
-
Test Across Browsers and Devices - Ensuring cross-browser and device compatibility is crucial for a smooth user experience. Testing on a variety of platforms helps identify potential issues early.
-
Implement Advanced Features as Needed - Depending on your requirements, you may want to add features like text search, annotations, or custom navigation controls. Nutrient provides a range of advanced tools for these features, allowing you to enhance the viewer’s functionality beyond the basics offered by PDF.js.
By following these best practices and choosing the right tools, you can build a robust and secure React PDF viewer that meets your application’s specific needs.
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.
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
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.
FAQ
Here are a few frequently asked questions about working with PDF.js to build a PDF viewer.
How can I build a PDF viewer in React.js using PDF.js?
You can build a PDF viewer in React.js by integrating the PDF.js library. This involves setting up PDF.js to render PDF documents and creating React components to display and interact with the PDF content.
How do I install and set up PDF.js in a React project?
Install PDF.js using npm, import it into your React components, and initialize it to load and render PDF files. You’ll also need to handle PDF.js worker setup and manage PDF rendering within the React lifecycle methods.
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.