Some test text!

Loading...
Guides
Sharing WebViewer instance across components

Sharing WebViewer instance across components

The problem

Webviewer can easily be instantiated inside a React component, however, the instance object returned from the WebViewer constructor is limited to the scope of the component its created in:

import {useEffect, useRef} from 'react';
import WebViewer from '@pdftron/webviewer'

const MyComponent = () => {
  const viewer = useRef(null);

  useEffect(() => {
    WebViewer(
      {
        path: '/webviewer/lib',
        initialDoc: '/files/pdftron_about.pdf',
      },
      viewer.current,
    ).then((instance) => {
         // We only have access to the instance here
      });
  }, []);

  return (
    <div className="MyComponent">
      <div className="webviewer" ref={viewer} style={{height: "100vh"}}></div>
    </div>
  );
};

In most React apps, you want to separate functionality on a per-component basis. For example, you may want a React component that loads a new document.

One way you could do this is by passing instance down as a prop to all of your components - but this can get messy and leads to prop drilling. Prop drilling occurs when you pass a prop down through multiple child components, and this pattern should be avoided because it leads to messy, hard to maintain code.

React context

To avoid prop drilling, we can use a powerful feature in React called context.

Context allows you to share data between components without the use of props. This is a great feature that can really clean up your code.

Let's use the context feature to share our WebViewer instance object with all of our components.

Creating the context

The first thing we need to do is create the context itself. This can be done with the createContext function. This context can live anywhere in your app, but for this example lets place it in src/context/webviewer.js

// src/context/webviewer.js 

import React from 'react'

export default WebViewerContext = React.createContext({});

Setting up the provider

Next, we need to provide our application with this context. You may be familiar with this concept if you have ever used libraries like Apollo, Redux, Chakra, etc.

To set up our provider, we simply wrap our entire app with the provider exported from our context:

// src/App.js

import WebViewerContext from 'src/context/webviewer.js '

export default function App() {
  return (
    <WebViewerContext.Provider>
      { /* ...Your application code here */}
    </WebViewerContext.Provider>
  )
}

Now, any components rendered inside this provider have access to the context.

We also have to provide some kind of value to our application. This value can be whatever you want, but for this example we will provide a way to get and set the WebViewer instance.

Expanding on our previous code, we provide set a getter and setter for the instance using the useState hook:

// src/App.js

import WebViewerContext from 'src/context/webviewer.js'
import { useState } from 'react'

export default function App() {

  const [instance, setInstance] = useState();

  return (
    <WebViewerContext.Provider value={{ instance, setInstance }}>
      { /* ...Your application code here */}
    </WebViewerContext.Provider>
  )
}

Now, every component in our app has access to both instance and setInstance.

Accessing and setting the context

The next step is setting the WebViewer instance object after WebViewer has been loaded. To do this, we need to call the setInstance function provided by our provider.

In our WebViewer component, we can gain access to this function with the useContext hook:

// src/components/WebViewer.js

import WebViewerContext from 'src/context/webviewer.js'
import { useEffect, useRef, useContext } from 'react';
import WebViewer from '@pdftron/webviewer'

export default function WebViewerComponent() {

  // useContext returns whatever "value" 
  // is provided by our provider we set up above
  const { setInstance } = useContext(WebViewerContext);

  const viewer = useRef(null);

  useEffect(() => {
    WebViewer(
      {
        path: '/webviewer/lib',
        initialDoc: '/files/pdftron_about.pdf',
      },
      viewer.current,
    ).then((instance) => {
      setInstance(instance) 
    });
  }, []);

  return (
    <div className="MyComponent">
      <div className="webviewer" ref={viewer} style={{height: "100vh"}}></div>
    </div>
  );
}

This code is loading WebViewer and mounting it to the DOM, and then calling the setInstance function provided from our provider. If everything is set up correctly, the instance state in App.js should now be set to the WebViewer instance, which means that our provider is now providing the WebViewer instance object to the rest of the application!

Using the instance

Now that the instance is set in our provider, every component now has access to it. This allows us to call WebViewer APIs without passing any props.

Let's create a LoadDocument component now using our new context:

// src/components/LoadDocument.js

import WebViewerContext from 'src/context/webviewer.js'
import { useEffect, useRef, useContext } from 'react';

export default function LoadDocument() {

   const { instance } = useContext(WebViewerContext)

   const load = () => {
      instance.UI.loadDocument('http://yourwebsite.com/file.pdf')
   }

   return (
     <button onClick={load}>Load document</button>
   )
}

You can see that this component now has access to the WebViewer instance with no props needed!

Cleaning up

We can clean up our code even further by creating a custom hook that just returns the instance. This prevents us from having to import the context everywhere we want to use it.

Let's create a custom hook called useInstance:

// src/hooks/useInstance

import WebViewerContext from 'src/context/webviewer.js'
import { useContext } from 'react';

export default function useInstance() {
  const { instance } = useContext(WebViewerContext);
  return instance;
}

This hook is very simple, but helps us clean up our code a bit. We no longer need to import WebViewerContext in all our components.

Let's go back and update our LoadDocument component:

// src/components/LoadDocument.js

import useInstance from 'src/hooks/useInstance'
import { useEffect, useRef } from 'react';

export default function LoadDocument() {

   const instance = useInstance();

   const load = () => {
      instance.UI.loadDocument('http://yourwebsite.com/file.pdf')
   }

   return (
     <button onClick={load}>Load document</button>
   )
}

As you can see, leveraging React context to share state across multiple components can really improve your code readability and prevents the need for prop drilling.

Get the answers you need: Support

UPCOMING WEBINAR: "2021 in review: Top five new & updated features" Dec 9th @ 11am PT