Building a Document Viewer with react-pdf
Tags: nextjs, pdf, tutorial, webdev
Published: 2023-07-26 17:51:16

## What you will find in this article?
PDF viewers have become essential components in many web applications. For instance, they are widely used in educational platforms, online libraries, and any other applications that involve document viewing. In this post, we will explore how we can create a beautiful page-by-page PDF viewer using `react-pdf`.

![Document GIF](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/3qe9v36qubpq12mmoac6.gif)

## Papermark - the open-source DocSend alternative.
Before we kick off, let me introduce you to Papermark. It's an open-source project for securely sharing documents that features a beautiful full-screen viewer for PDF documents.

I would be absolutely thrilled if you could give us a star! Don't forget to share your thoughts in the comments section ❤️

https://github.com/mfts/papermark

![Papermark Analytics](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/1gzfhi5hp28ckx7tpday.gif)

## Setup the project
Let's set up our project environment. We will be setting up a Next.js app and installing the required libraries.

### Set up tea
It's a good idea to have a package manager handy, like [`tea`](https://github.com/teaxyz/cli). It'll handle your development environment and simplify your (programming) life!

```sh
sh <(curl https://tea.xyz)

# --- OR ---
# using brew
brew install teaxyz/pkgs/tea-cli
```

`tea` frees you to focus on your code, as it takes care of installing `node`, `npm`, `vercel` and any other packages you may need. The best part is, `tea` installs all packages in a dedicated directory (_default: `~/.tea`_), keeping your system files neat and tidy.

### Set up Next.js with TypeScript and Tailwindcss
We will use create-next-app to generate a new Next.js project. We will also be using TypeScript and Tailwind CSS, so make sure to select those options when prompted.

```bash
npx create-next-app

# ---
# you'll be asked the following prompts
What is your project named?  my-app
Would you like to add TypeScript with this project?  Y/N
# select `Y` for typescript
Would you like to use ESLint with this project?  Y/N
# select `Y` for ESLint
Would you like to use Tailwind CSS with this project? Y/N
# select `Y` for Tailwind CSS
Would you like to use the `src/ directory` with this project? Y/N
# select `N` for `src/` directory
What import alias would you like configured? `@/*`
# enter `@/*` for import alias
```

### Install react-pdf
There are actually **two** npm packages called "react-pdf": one is for displaying PDFs and one is for generating PDFs. Today we are focusing on the one to generate PDFs: [https://github.com/wojtekmaj/react-pdf](https://github.com/wojtekmaj/react-pdf).


```bash
# Navigate to your Next.js repo
cd my-app

# Install react-pdf
npm install react-pdf
```

### Building the application
Now that we have our setup in place, we are ready to start building our application.
  
### Set up the PDF Viewer
The ability to programmatically configure pipes and datasources for Tinybird offers a significant advantage. This flexibility enables us to treat our data infrastructure as code, meaning that the entire configuration can be committed into a version control system. For an open-source project like Papermark, this capability is highly beneficial. It fosters transparency and collaboration, as contributors can readily understand the data structure without any ambiguity.

We set up Tinybird pipes and datasource as follows:

```tsx
// components/pdfviewer.tsx
import { ChevronLeftIcon, ChevronRightIcon } from "@heroicons/react/20/solid";
import { useEffect, useRef, useState } from "react";
import { Document, Page, pdfjs } from "react-pdf";

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

export default function PDFViewer(props: any) {
  const [numPages, setNumPages] = useState<number>(0);
  const [pageNumber, setPageNumber] = useState<number>(1); // start on first page
  const [loading, setLoading] = useState(true);
  const [pageWidth, setPageWidth] = useState(0);

  function onDocumentLoadSuccess({
    numPages: nextNumPages,
  }: {
    numPages: number;
  }) {
    setNumPages(nextNumPages);
  }

  function onPageLoadSuccess() {
    setPageWidth(window.innerWidth);
    setLoading(false);
  }

  const options = {
    cMapUrl: "cmaps/",
    cMapPacked: true,
    standardFontDataUrl: "standard_fonts/",
  };

  // Go to next page
  function goToNextPage() {
    setPageNumber((prevPageNumber) => prevPageNumber + 1);
  }

  function goToPreviousPage() {
    setPageNumber((prevPageNumber) => prevPageNumber - 1);
  }


  return (
    <>
      <Nav pageNumber={pageNumber} numPages={numPages} />
      <div
        hidden={loading}
        style={{ height: "calc(100vh - 64px)" }}
        className="flex items-center"
      >
        <div
          className={`flex items-center justify-between w-full absolute z-10 px-2`}
        >
          <button
            onClick={goToPreviousPage}
            disabled={pageNumber <= 1}
            className="relative h-[calc(100vh - 64px)] px-2 py-24 text-gray-400 hover:text-gray-50 focus:z-20"
          >
            <span className="sr-only">Previous</span>
            <ChevronLeftIcon className="h-10 w-10" aria-hidden="true" />
          </button>
          <button
            onClick={goToNextPage}
            disabled={pageNumber >= numPages!}
            className="relative h-[calc(100vh - 64px)] px-2 py-24 text-gray-400 hover:text-gray-50 focus:z-20"
          >
            <span className="sr-only">Next</span>
            <ChevronRightIcon className="h-10 w-10" aria-hidden="true" />
          </button>
        </div>

        <div className="h-full flex justify-center mx-auto">
          <Document
            file={props.file}
            onLoadSuccess={onDocumentLoadSuccess}
            options={options}
            renderMode="canvas"
            className=""
          >
            <Page
              className=""
              key={pageNumber}
              pageNumber={pageNumber}
              renderAnnotationLayer={false}
              renderTextLayer={false}
              onLoadSuccess={onPageLoadSuccess}
              onRenderError={() => setLoading(false)}
              width={Math.max(pageWidth * 0.8, 390)}
            />
          </Document>
        </div>
      </div>
    </>
  );
}


function Nav({pageNumber, numPages}: {pageNumber: number, numPages: number}) {
  return (
    <nav className="bg-black">
      <div className="mx-auto px-2 sm:px-6 lg:px-8">
        <div className="relative flex h-16 items-center justify-between">
          <div className="flex flex-1 items-center justify-center sm:items-stretch sm:justify-start">
            <div className="flex flex-shrink-0 items-center">
              <p className="text-2xl font-bold tracking-tighter text-white">
                Papermark
              </p>
            </div>
          </div>
          <div className="absolute inset-y-0 right-0 flex items-center pr-2 sm:static sm:inset-auto sm:ml-6 sm:pr-0">
            <div className="bg-gray-900 text-white rounded-md px-3 py-2 text-sm font-medium">
              <span>{pageNumber}</span>
              <span className="text-gray-400"> / {numPages}</span>
            </div>
          </div>
        </div>
      </div>
    </nav>
  );
}
```

Let's break it down what's happening here:

From `react-pdf`, we are using Document and Page component. In addition, we are loading the pre-packaged version of pdfjs to initialize a worker in the browser. 

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

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

Next, we are writing two function that 1) count the total number of pages (once) and 2) adjust the page width as needed (per page)

```tsx
// ...
export default function PDFViewer(props: any) {
  // useState variables

  function onDocumentLoadSuccess({
    numPages: nextNumPages,
  }: {
    numPages: number;
  }) {
    setNumPages(nextNumPages);
  }

  function onPageLoadSuccess() {
    setPageWidth(window.innerWidth);
    setLoading(false);
  }

  // ...
}
```

Next, we use the `Document` and `Page` components. This is the core of the PDF viewer. Important to note that `Document` takes a `file` prop, that can be a URL, base64 content, Uint8Array, and more. In my case, I'm loading a URL to a file.

```tsx
// ...
export default function PDFViewer(props: any) {
  // ...
  return (
    <div className="h-full flex justify-center mx-auto">
      <Document
        file={props.file}
        onLoadSuccess={onDocumentLoadSuccess}
        options={options}
        renderMode="canvas"
        className=""
      >
        <Page
          className=""
          key={pageNumber}
          pageNumber={pageNumber}
          renderAnnotationLayer={false}
          renderTextLayer={false}
          onLoadSuccess={onPageLoadSuccess}
          onRenderError={() => setLoading(false)}
          width={Math.max(pageWidth * 0.8, 390)}
        />
      </Document>
    </div>
  )
}
```

We also added a navigation bar with showing the current page number and buttons for navigating to the next / previous page of the document.

You can use the PDF Viewer component anywhere in your application to visualize PDFs beautifully.

Tada 🎉 The PDF Viewer component is ready! 

![PDF Viewer in application](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/astanv6wlslihob20nyo.png)

## Conclusion
That's it! We've built a beautiful PDF Viewer component for displaying PDF documents using react-pdf, and Next.js.

Thank you for reading. I'm Marc, an open-source advocate. I am building [papermark.io](https://www.papermark.io/) - the open-source alternative to DocSend with millisecond-accurate page analytics.

## Help me out!
If you found this article helpful and got to understand react-pdf better, I would be eternally grateful if you could give us a star! And don't forget to share your thoughts in the comments ❤️

https://github.com/mfts/papermark

![Cat GIF](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/51c5ueoki0jgn8i8ukkz.gif)