Create PDF viewer in React JS with Zoom and Pagination

A PDF file displays read-only documents with text, images, and links. The PDF files are generally accessed through third-party applications such as Adobe reader, etc, and rendering PDF files on a web application is not supported natively. Hence, we will use external npm packages to accomplish the following functionalities:

  1. Display the PDF file on the screen.
  2. Feature to Zoom in and Zoom out of a document on button click.
  3. Allow users to navigate between the previous and next pages.
React JS PDF Viewer
PDF Viewer created using React JS
Create PDF Viewer in React JS
Create PDF Viewer in React JS

1) Install “react-pdf” npm package

The “react-pdf” name package provides utility functions to render a PDF file along with additional features such as pagination and page scale.

npm i react-pdf

// For projects configured with yarn 
yarn add react-pdf

2) Enable pdf.js worker to process file content

PDF.js is a javascript library that allows a browser-based application to render PDF files and is importable from “react-pdf” package.

import { pdfjs } from "react-pdf";

To process PDF files pdf.js requires a worker.js file that can be accessed through the global CDNs.

pdfjs.GlobalWorkerOptions.workerSrc = `//cdnjs.cloudflare.com/ajax/libs/pdf.js/${pdfjs.version}/pdf.worker.js`;

3) Access PDF file in React project

There are two approaches to accessing a file inside React JS project:

  1. Include a PDF file inside /public directory of the project, once included the file can be directly referenced using the file name.
  2. Specify the URL where the PDF file is hosted which requires bypassing the CORS security.
const url =
  "https://cors-anywhere.herokuapp.com/https://ncu.rcnpv.com.tw/Uploads/20131231103232738561744.pdf";

4) Load the PDF document

We declare a React state “totalPages” to maintain the count of total pages in the PDF file. The value for “totalPages” is initially “0” and it’s updated after the document is loaded.

const [totalPages, setTotalPages] = useState(0);

function onDocumentLoadSuccess({ numPages }) {
  setNumPages(numPages);
}

The <Document/> component provides direct access to the specified file for the child components.

import { Document } from "react-pdf";
<Document file="sampleFile.pdf" onLoadSuccess={onDocumentLoadSuccess}>
</Document>

5) Display file using <Page/> component

import { Document, Page, pdfjs } from "react-pdf";

The PDF file is controlled using the following React states:

  1. pageNumber: Indicates the current page number of the file.
  2. pageScale: Number that indicates how much the PDF file is Zoomed.
const [pageNumber, setPageNumber] = useState(1);
const [pageScale, setPageScale] = useState(1);

The PDF file must be displayed page by page, hence <Page/> component is used to render an individual page in the document.

<Document file="sampleFile.pdf" onLoadSuccess={onDocumentLoadSuccess}>
  <Page pageNumber={pageNumber} scale={pageScale} />
</Document>

6) Zoom In/Zoom out of the PDF file

The document is zoomed in/out by incrementing/decrementing the page scale. Additionally, an upper and lower limit of “pageScale” state is required to ensure that the application doesn’t break.

function handleZoomIn() {
  if (pageScale < 3) {
    setPageScale(pageScale + 0.2);
  }
}

function handleZoomOut() {
  if (pageScale > 0.3) {
    setPageScale(pageScale - 0.2);
  }
}
<div className="button-container">
  <button onClick={handleZoomIn} disabled={pageScale >= 3}>
    Zoom +
  </button>
  <button onClick={handleZoomOut} disabled={pageScale <= 0.3}>
    Zoom -
  </button>
</div>

7) Pagination with next and previous buttons

The page of the document is updated dynamically using “setPageNumber()” method. It is important to limit page numbers between 1 to the total no of pages for ensuring multiple quick button clicks are ignored once the start/end point is reached.

function handleNext() {
  if (pageNumber < totalPages) {
    setPageNumber(pageNumber + 1);
  }
}
function handlePrevious() {
  if (pageNumber > 0) {
    setPageNumber(pageNumber - 1);
  }
}

Disabling buttons when previous and next page actions cannot be performed improves the user experience.

<div className="page-text">
  Page {pageNumber} of {totalPages}
</div>
<div className="button-container">
  <button onClick={handlePrevious} disabled={pageNumber === 1}>
    ‹ Previous
  </button>
  <button onClick={handleNext} disabled={pageNumber === totalPages}>
    Next ›
  </button>
</div>

Final Solution Code

App.js

import React, { useState } from "react";
import { Document, Page, pdfjs } from "react-pdf";

import "./styles.css";

export default function App() {
  pdfjs.GlobalWorkerOptions.workerSrc = `//cdnjs.cloudflare.com/ajax/libs/pdf.js/${pdfjs.version}/pdf.worker.js`;
  const [totalPages, setTotalPages] = useState(0);
  const [pageNumber, setPageNumber] = useState(1);
  const [pageScale, setPageScale] = useState(1);

  const url =
    "https://cors-anywhere.herokuapp.com/https://ncu.rcnpv.com.tw/Uploads/20131231103232738561744.pdf";

  function onDocumentLoadSuccess({ numPages }) {
    setTotalPages(numPages);
  }

  function handleZoomIn() {
    if (pageScale < 3) {
      setPageScale(pageScale + 0.2);
    }
  }

  function handleZoomOut() {
    if (pageScale > 0.3) {
      setPageScale(pageScale - 0.2);
    }
  }

  function handleNext() {
    if (pageNumber < totalPages) {
      setPageNumber(pageNumber + 1);
    }
  }
  function handlePrevious() {
    if (pageNumber > 0) {
      setPageNumber(pageNumber - 1);
    }
  }
  return (
    <div className="App">
      <div className="page-container">
        <Document file="sampleFile.pdf" onLoadSuccess={onDocumentLoadSuccess}>
          <Page pageNumber={pageNumber} scale={pageScale} />
        </Document>
      </div>
      <div className="footer">
        <div className="button-container">
          <button onClick={handleZoomIn} disabled={pageScale >= 3}>
            Zoom +
          </button>
          <button onClick={handleZoomOut} disabled={pageScale <= 0.3}>
            Zoom -
          </button>
        </div>
        <div className="page-text">
          Page {pageNumber} of {totalPages}
        </div>
        <div className="button-container">
          <button onClick={handlePrevious} disabled={pageNumber === 1}>
            ‹ Previous
          </button>
          <button onClick={handleNext} disabled={pageNumber === totalPages}>
            Next ›
          </button>
        </div>
      </div>
    </div>
  );
}

styles.css

.App {
  height: 100vh;
  display: flex;
  justify-content: center;
  align-items: center;
  flex-direction: column;
}
.page-container {
  margin: 20px;
  width: 650px;
  height: 700px;
  overflow-y: scroll;
  box-shadow: 0 25px 50px 0 rgba (62, 62, 62, 0.15);
  -webkit-box-shadow: 0 25px 50px 0 rgb(62 62 62 / 15%);
}
.footer {
  display: flex;
  justify-content: space-between;
  width: 500px;
}
.button-container {
  display: flex;
  gap: 8px;
}