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:

JavaScript

1import {useEffect, useRef} from 'react';
2import WebViewer from '@pdftron/webviewer'
3
4const MyComponent = () => {
5 const viewer = useRef(null);
6
7 useEffect(() => {
8 WebViewer(
9 {
10 path: '/webviewer/lib',
11 initialDoc: 'https://pdftron.s3.amazonaws.com/downloads/pl/demo-annotated.pdf',
12 },
13 viewer.current,
14 ).then((instance) => {
15 // We only have access to the instance here
16 });
17 }, []);
18
19 return (
20 <div className="MyComponent">
21 <div className="webviewer" ref={viewer} style={{height: "100vh"}}></div>
22 </div>
23 );
24};

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

JavaScript

1// src/context/webviewer.js
2
3import React from 'react'
4
5export 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:

JavaScript

1// src/App.js
2
3import WebViewerContext from 'src/context/webviewer.js '
4
5export default function App() {
6 return (
7 <WebViewerContext.Provider>
8 { /* ...Your application code here */}
9 </WebViewerContext.Provider>
10 )
11}

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:

JavaScript

1// src/App.js
2
3import WebViewerContext from 'src/context/webviewer.js'
4import { useState } from 'react'
5
6export default function App() {
7
8 const [instance, setInstance] = useState();
9
10 return (
11 <WebViewerContext.Provider value={{ instance, setInstance }}>
12 { /* ...Your application code here */}
13 </WebViewerContext.Provider>
14 )
15}

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:

JavaScript

1// src/components/WebViewer.js
2
3import WebViewerContext from 'src/context/webviewer.js'
4import { useEffect, useRef, useContext } from 'react';
5import WebViewer from '@pdftron/webviewer'
6
7export default function WebViewerComponent() {
8
9 // useContext returns whatever "value"
10 // is provided by our provider we set up above
11 const { setInstance } = useContext(WebViewerContext);
12
13 const viewer = useRef(null);
14
15 useEffect(() => {
16 WebViewer(
17 {
18 path: '/webviewer/lib',
19 initialDoc: '/files/pdftron_about.pdf',
20 },
21 viewer.current,
22 ).then((instance) => {
23 setInstance(instance)
24 });
25 }, []);
26
27 return (
28 <div className="MyComponent">
29 <div className="webviewer" ref={viewer} style={{height: "100vh"}}></div>
30 </div>
31 );
32}

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:

JavaScript

1// src/components/LoadDocument.js
2
3import WebViewerContext from 'src/context/webviewer.js'
4import { useEffect, useRef, useContext } from 'react';
5
6export default function LoadDocument() {
7
8 const { instance } = useContext(WebViewerContext)
9
10 const load = () => {
11 instance.UI.loadDocument('http://yourwebsite.com/file.pdf')
12 }
13
14 return (
15 <button onClick={load}>Load document</button>
16 )
17}

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:

JavaScript

1// src/hooks/useInstance
2
3import WebViewerContext from 'src/context/webviewer.js'
4import { useContext } from 'react';
5
6export default function useInstance() {
7 const { instance } = useContext(WebViewerContext);
8 return instance;
9}

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:

JavaScript

1// src/components/LoadDocument.js
2
3import useInstance from 'src/hooks/useInstance'
4import { useEffect, useRef } from 'react';
5
6export default function LoadDocument() {
7
8 const instance = useInstance();
9
10 const load = () => {
11 instance.UI.loadDocument('http://yourwebsite.com/file.pdf')
12 }
13
14 return (
15 <button onClick={load}>Load document</button>
16 )
17}

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.

Did you find this helpful?

Trial setup questions?

Ask experts on Discord

Need other help?

Contact Support

Pricing or product questions?

Contact Sales