Blog Post

How to build a React PDF viewer with PDF.js

Illustration: How to build a React PDF viewer with PDF.js
Information

This article was first published in November 2021 and was updated in August 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.

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

  1. 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
  1. 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.
}

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:

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

  1. Setting up the viewer

  • In the app/page.jsx file, use React’s useEffect and useRef hooks to load the Nutrient viewer within a div 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 the public directory, making it easily accessible to the viewer. The viewer loads this document when the component is mounted, and the baseUrl 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;
  1. 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.

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:

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.

Authors
Hulya Masharipov Technical Writer

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

Ritesh Kumar Web 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.

Related products
Share post
Free trial Ready to get started?
Free trial