PDFTron is now Apryse. Same great products, new name.

How to Build a React PDF Viewer with PDF.js

By Logan Bittner | 2018 Oct 17

Sanity Image
Read time

8 min

React components are chunks of isolated code you can easily share across your entire UI, and even across multiple projects. This post describes how to create a React PDF.js viewer component you can use in your projects to open and display a PDF file in React. We will also cover an abstraction technique you can use to help future-proof your code.

We offer related guides for those interested in how to open PDF and Office files in React or how to create a Next JS PDF viewer.

Types of Open-Source React PDF Libraries

There are numerous quality open-source React PDF.js libraries available. One popular library, with more than 400,000 weekly downloads on NPM, is React PDF. Built on top of PDF.js, this is a good place to look for a simple, fast way to view existing PDFs. To learn more about this library, refer to our comparison guide blog post.

For this tutorial, we’ll focus on Create React App. This library isn’t quite as popular as React PDF, but with 100,000 weekly downloads on NPM, we like it because it has everything you need to build a modern, single-page React app.

Some Create React App features include:

  • Syntax support for React, JSX, ES6, TypeScript, and Flow.
  • An interactive unit test runner.
  • A live development server that warns about common mistakes.
  • A build script to bundle JS, CSS, and images for production, with hashes and sourcemaps.

Requirements to Get Started

Before starting on this project, you need Node 14.0.0 or a later version on your local development machine.

Step 1 - Install Dependencies

First of all, we need an environment to create our component in. For this PDF.js React example, we will use create-react-app to generate a project. Navigate to an empty directory and run the following command:

npm i create-react-app --save-dev

Step 2 - Create Your React JS PDF Viewer Project

Use the following commands to generate your PDF.js React project:

npx create-react-app my-app
cd my-app

This installs React and any other dependencies you need. Now you can start your local server by running:

npm run start

Navigate to http://localhost:3000/ and you should see the default create-react-app welcome screen.

Step 3 - Project Setup

Next we need to set up our project. First, let’s start by clearing the templates that create-react-app provides. Navigate to src/App.js and delete all the code inside the render function, except the outer div. The file should now contain:

import React, { Component } from 'react';
import './App.css';

class App extends Component {
  render() {
    return (
      <div className="App">

      </div>
    );
  }
}

export default App;

We can also delete any other unused code. Delete src/logo.svg and App.test.js, and replace all the css in App.css with the following snippet:

html, body, #root, .App {
  width: 100%;
  height: 100%;
}

Now we need to create our PDF viewing component that will let us show a PDF in React. Let’s create the file src/components/PDFViewer/PDFViewer.js and start by writing a basic React component.

import React from 'react';

export default class PDFViewer extends React.Component {

  render() {
    return (
      <div id='viewer' style={{ width: '100%', height: '100%' }}>
        Hello world!
      </div>
    )
  }
}

Now, back in App.js, let's import this component and render it. App.js should now look like this:

import React, { Component } from 'react';
import './App.css';
import PDFViewer from './components/PDFViewer/PDFViewer';

class App extends Component {
  render() {
    return (
      <div className="App">
        <PDFViewer />
      </div>
    );
  }
}

export default App;

If you take a look at http://localhost:3000, you should see a little “Hello world!” on a white background.

Step 4 - Defining Our Backend to Render Pages in React

Now it’s time to start adding some functionality!

Instead of hardcoding PDF functionality right inside our React component, we will use a more abstracted approach and pass a class that implements a set of functions to the component. We call these classes 'backends', and they help the component render the PDF. Using this approach makes our React component even more reusable and future proof.

Let’s create our first backend that renders a PDF using PDF.js. Create the file src/backends/pdfjs.js and export a class that contains an init function.

export default class PDFJs {
  init = () => {
    
  }
}

Most PDF libraries require you to pass a DOM node to render the PDF inside. We also need to know which PDF to render in the first place. Let’s make the init function accept both of these things as parameters.

export default class PDFJs {
  init = (source, element) => {
    
  }
}

Before we start implementing PDF rendering, let’s get this backend hooked up to our component. In App.js, import the pdfjs.js backend and pass it as a prop to our PDFViewer component. We’re also going to need a PDF source, so let's pass that as well. App.js should now look something like this:

import React, { Component } from 'react';
import './App.css';
import PDFViewer from './components/PDFViewer/PDFViewer';
import PDFJSBackend from './backends/pdfjs';

class App extends Component {
  render() {
    return (
      <div className="App">
        <PDFViewer 
          backend={PDFJSBackend}
          src='/myPDF.pdf'
        />
      </div>
    );
  }
}

export default App;

Now in our PDFViewer component, let's implement the backend's init function. First we start by creating an instance of the backend and storing it to the component.

import React from 'react';

export default class PDFViewer extends React.Component {
  constructor(props) {
    super(props);
    this.viewerRef = React.createRef();
    this.backend = new props.backend();
  }

  render() {
    return (
      <div ref={this.viewerRef} id='viewer' style={{ width: '100%', height: '100%' }}>

      </div>
    )
  }
}

Since we need reference to a DOM node inside our init function, we call it inside componentDidMount (DOM nodes are guaranteed to be present when componentDidMount is called). We pass it a reference to the #viewer div (using React refs), as well as the PDF source. We can also remove the "Hello world" from the render function while we're at it. PDFViewer.js should now look like this:

import React from 'react';

export default class PDFViewer extends React.Component {
  constructor(props) {
    super(props);
    this.viewerRef = React.createRef();
    this.backend = new props.backend();
  }

  componentDidMount() {
    const { src } = this.props;
    const element = this.viewerRef.current;

    this.backend.init(src, element);
  }
  

  render() {
    return (
      <div ref={this.viewerRef} id='viewer' style={{ width: '100%', height: '100%' }}>

      </div>
    )
  }
}

The final step is making our init function actually do something. Let's test it out by making it render some text.

export default class PDFJs {
  init = (source, element) => {
    const textNode = document.createElement('p');
    textNode.innerHTML = `Our PDF source will be: ${source}`;

    element.appendChild(textNode);
  }
}

http://localhost:3000 should now display "Our PDF source will be: /myPDF.pdf".

Step 5 - Implementing PDF.js

We start by using the open sourced PDF.js library to render a PDF. Download the library from https://mozilla.github.io/pdf.js/getting_started/#download and extract the contents inside the public folder of our project.

We also need a PDF file to view. You can download one, or use your own. Place it in the public folder as well.

Your overall project structure now looks something like this (your version number of PDF.js may be different — this is okay):

VS Code screenshot

We implement PDF.js UI by using an iframe to point to its viewer file. In our init function, we create an iframe and set its source to the PDF.js UI, and we use query params to tell the UI which PDF to render. Once the iframe is created, we append it to the DOM element passed into the init function.

src/backends/pdfjs.js should now look like this:

export default class PDFJs {
  init = (source, element) => {
    const iframe = document.createElement('iframe');

    iframe.src = `/pdfjs-1.9.426-dist/web/viewer.html?file=${source}`;
    iframe.width = '100%';
    iframe.height = '100%';

    element.appendChild(iframe);
  }
}

That's it! If you check out http://localhost:3000, you should see your PDF displayed inside the PDF viewer in React.

Viewing a PDF using PDF.js is fairly easy, but once you want to start annotating or using more advanced features such as real-time collaboration, redaction, or page manipulation, you must implement these things yourself. See our Guide to Evaluating PDF.js to learn more.

That's where Apryse comes in. Our WebViewer library provides all these features out of the box, with zero configuration. WebViewer also has far better render accuracy in many cases.

Implementing a React PDF Viewer with WebViewer

WebViewer is Apryse's JavaScript PDF Viewer, which allows you to open PDFs, view Office docs, and many other file formats right in the browser — view a demo.

Start by downloading and importing the WebViewer dependencies into our project. Then, extract the contents into /public.

Now import the files by adding the following code in public/index.html right before </head>.

<script src='/WebViewer/lib/webviewer.min.js'></script>

We are now ready to create our WebViewer backend. Create the file src/backends/webviewer.js, and export a class with an init function that accepts a PDF source and a DOM element. (You can probably see where this is going!)

Copy this WebViewer constructor code into the init function:

export default class PDFTron {
  init = (source, element) => {
    new window.PDFTron.WebViewer({
      path: '/WebViewer/lib',
      initialDoc: source,
    }, element);
  }
}

Now is when all our hard work pays off! To switch our app to use WebViewer instead of PDF.js, just pass a different backend to our component. In App.js, replace the PDF.js backend with our new WebViewer backend.

import React, { Component } from 'react';
import './App.css';
import PDFViewer from './components/PDFViewer/PDFViewer';
import PDFJSBackend from './backends/pdfjs';
import WebviewerBackend from './backends/webviewer';

class App extends Component {
  render() {
    return (
      <div className="App">
        <PDFViewer backend={WebviewerBackend} src='/myPDF.pdf' />
      </div>
    );
  }
}

export default App;

And that's it! Our PDFViewer component doesn't require any changes because it already knows to call the init function when the component is mounted.

Extending the Backends (Optional)

Let's say we want to implement a programmatic rotate feature into our component.

Start by adding a rotate function into our WebViewer backend.

export default class Webviewer {
  init = (source, element) => {
    new window.PDFTron.WebViewer({
      path: '/WebViewer/lib',
      initialDoc: source,
    }, element);
  }

  rotate = (direction) => {

  }
}

We also need reference to our WebViewer instance, so let's save this as part of the instance in our init function.

export default class Webviewer {
  init = (source, element) => {
    this.viewer = new window.PDFTron.WebViewer({
      path: '/WebViewer/lib',
      initialDoc: source,
    }, element);
  }


  rotate = (direction) => {

  }
}

We can now use the WebViewer rotateClockwise and rotateCounterClockwise functions to rotate the current document. For more information, see the documentation.

export default class Webviewer {
  init = (source, element) => {
    this.viewer = new window.PDFTron.WebViewer({
      path: '/WebViewer/lib',
      initialDoc: source,
    }, element);
  }


  rotate = (direction) => {
    if(direction === 'clockwise') {
      this.viewer.rotateClockwise();
    } else {
      this.viewer.rotateCounterClockwise();
    }
  }
}

Now, we just need to implement this into our PDFViewer component.

import React from 'react';

export default class PDFViewer extends React.Component {

  constructor(props) {
    super(props);
    this.viewerRef = React.createRef();
    this.backend = new props.backend();
  }

  componentDidMount() {
    const { src } = this.props;
    const element = this.viewerRef.current;
    this.backend.init(src, element);
  }

  rotate = (direction) => {
    // check to make sure the backend has this function implemented
    if (this.backend.rotate) {
      this.backend.rotate(direction);
    }
  }

  render() {
    return (
      <div ref={this.viewerRef} id='viewer' style={{ width: '100%', height: '100%' }}>

      </div>
    )
  }
}

That's it! You could now implement a rotate function to your PDF.Js backend if you wanted.

To use this rotate function, we can get a reference to our PDFViewer component and call the rotate function directly.

import React, { Component } from 'react';
import './App.css';
import PDFViewer from './components/PDFViewer/PDFViewer';
import PDFJSBackend from './backends/pdfjs';
import WebviewerBackend from './backends/webviewer';

class App extends Component {

  constructor() {
    super();
    this.myViewer = React.createRef();
  }

  onButtonClick = () => {
    this.myViewer.current.rotate('clockwise');
  }

  render() {
    return (
      <div className="App">

        <button onClick={this.onButtonClick}>Rotate Clockwise</button>

        <PDFViewer
          ref={this.myViewer}
          backend={WebviewerBackend}
          src='/myPDF.pdf'
        />

      </div>
    );
  }
}

export default App;

Conclusion

As you can see, viewing a PDF inside a React app isn't tough using open source software. If you need more features, Apryse WebViewer is just as simple to implement but gives you advanced functionality built right in:

We also saw how abstracting the functionality allows us to future-proof our code and allow us to easily switch from one viewer to another without touching our components.

For more information, see the full source code for React PDF viewer example. View a full demo of WebViewer and feel free to compare it to the PDF.js viewer.

Also, check out our React PDF generator post. or read up on the Apryse PDF SDK.

If you have any questions about implementing WebViewer in your project, please contact us and we will be happy to help!

Sanity Image

Logan Bittner

Share this post

email
linkedIn
twitter