Available Now: Explore our latest release with enhanced accessibility and powerful IDP features

How Duplicate WebViewer Instances Trigger Warnings

By Roger Dunham | 2024 Jan 05

Sanity Image
Read time

8 min

Seeing errors in your React application should always make you ask questions.

In this article we look at the reason why you may see the error “Two instances of WebViewer were created on the same HTML element” when you use the Apryse WebViewer with React 18.

Introduction

Copied to clipboard

React is a great, open-source, front-end, JavaScript library for building interactive user interfaces based on components. It’s been around since 2013 and is used by hundreds of thousands of developers.

With that level of popularity, Apryse have created multiple sample projects that use React to demonstrate the Apryse WebViewer and its ability to view and edit PDFs and Office documents with no server-side dependencies.

There is a disadvantage of being a popular framework though. As React is being actively developed, the way that it works changes with each version.

 

In this article, we will look at, and offer solutions to, one problem that is associated with the use of React 18 – multiple firing of useEffect functions in development mode, which can lead to the error “Two instances of WebViewer were created on the same HTML element”.

 

What The Problem Looks Like

Copied to clipboard

The first time that I tried to embed WebViewer into a React application I was confronted by an error as soon as I tried to run the program.

Blog image

Figure 1 - A typical message that may be seen when using WebViewer in a React 18 project.

 

I’ve worked with several programming languages but I haven’t much experience with React, and therefore I assumed that I was at fault. I suspect that many other people feel exactly the same way when they face messages like this.

I went through the video tutorial again and found that I had copied the WebViewer lib files one level too deeply into my projects public folder. I fixed that, then pressed ‘Refresh’ in the browser, the file reloaded, and I could see and work with WebViewer, and do all of the cool things that it supports – adding annotations, editing the PDF, and so on.

And I had also made a crucial error in my analysis. I convinced myself that it was my code change that was the issue – but in fact, the reason that it worked when I refreshed the browser was that the refresh itself was making the difference.

In fact, if all I had done was press reload in the first place then it would still have worked.

Since then, I have worked on many other sets of sample code that use WebViewer. They all exhibit the problem in some way or another, but the details of the error change depending on the way in which the app was created.

An app created using create-react-app shows an error the first time that it is loaded, but disappears on a refresh (which was exactly what had happened to me the first time that I used WebViewer).

Vite, on the other hand, created an app that appeared to work first time. It was only when I looked at the Developer tools that I realized that there was an error, but that it was being silently suppressed.

Blog image

Figure 2 - The error shown in the console log for an app generated using Vite and React 18.

Different again was an app based on NextJS-14, which uses React for the client-side rendering. In that case an error dialog is shown even after a refresh, and yet WebViewer renders and behaves correctly.

Blog image

Figure 3 - Output from a NextJS14 app that is using React 18 - an error message is shown in the UI.

However, if the package.json file is modified to use React 17 (and App.tsx updated to work with that version rather than React 18), then the problem does not occur.

Blog image

Figure 4 - Exactly the same app, using React 17. No error occurs.    

So, what on earth is going on? Does it matter? Can we fix it? Can we hide it?

The Underlying Cause of The Problem

Copied to clipboard

The sample code for all the React-based apps initialize the WebViewer component in a useEffect hook which has an empty dependency list. Typical code is as follows.

  useEffect(()=>{ 
    WebViewer({ 
    path: 'lib', 
     initialDoc: 'https://pdftron.s3.amazonaws.com/downloads/pl/webviewer-demo.pdf' 
  }, viewerDiv.current).then(); 
  },[]) 

The whole point of the useEffect hook with an empty dependency list, is that the hook should fire just once when the component mounts.

And that is exactly what happens in Production builds of the project.

However, in Development builds something special happens – the hook fires twice as the component mounts.

This is a React 18 change of behavior and is deliberate.

There are scenarios where a resource, such as a database connection, is acquired during a useEffect as a component mounts, and that resource should be freed when the component unmounts. Forcing the useEffect to fire twice in Dev builds makes it more obvious that clean-up is not occurring and that some kind of resource leak may occur.

If you want to try it yourself then it is quite easy to prove that the useEffect is fired twice. Simple create a new React 18 app and add a useEffect which contains logging. Example code is shown below.

function App() { 
  const [startTime, setStartTime]=useState<string>(); 
  useEffect(() => { 
    console.log("About to get time"); 
    const s= (new Date().getTime().toLocaleString()); 
    setStartTime(s); 
    console.log("Stored the time at " + s); 
    return () => {  
      console.log("About to clear time"); 
      setStartTime("Unknown"); 
    }; 
  }, []); 

  return ( 
    <div className="App"> 
    This started at {startTime} 
    </div> 
  ); 
} 

This simple app gets the number of milliseconds since midnight Jan 1st, 1970 when the page first loads, and displays it.

In a Development build you can see from the console log that the code within the useEffect is called, then the return function is also called, then the useEffect is called again a millisecond or so later.

Blog image

  Figure 5 - Output of a simple program that gets the number of milliseconds since 1st Jan 1970. The useEffect is being called twice when the app is run as a Development build.

But if we take exactly the same code, create a production build, then run that using serve -s build, we get a different result. This time the useEffect is called only once - exactly as we expect.

Blog image

Figure 6 - The same source code, but when run as a production build the useEffect is called only once.

In summary – with React 18 - for Production builds, the useEffect is being called once, which is exactly what is expected, but in a Development build it is called once, then the return function is called, then it is called again.

 

This is a change in behavior since React 17, and is caused by changes in the way that Strict Mode works. A ReactJS blog says:

The Apryse React samples require that the WebViewer is initialised in the useEffect and assigned to the referenced div element. This behavior change means that the useEffect is called twice, and two WebViewers are assigned to the same element.

Options for How to Work With These Warnings When Using Apryse WebViewer

Copied to clipboard

Just Ignore the Warnings

These warning only occur because of an artificial difference in Dev builds. Since you won’t be deploying Dev builds they can be ignored.

The disadvantage of this, is that when we learn to ignore one set of warnings, even in Dev builds, we tend towards ignoring all warnings – and then we find that something breaks that we had been warned about for months.

Use an Earlier Version of React

The multiple firing of useEffect functions was introduced in React 18.

Downgrading to an earlier version of React will avoid that. Downgrading is not straightforward though, and if your code base is already using React 18  then changes in behavior between React versions may result in significant, and frustrating, code changes being required.

In reality, downgrading may not be a realistic option. However, this issue is an argument for not migrating existing code from a previous version to React 18.

Disable Strict Mode in React

Strict Mode is typically applied by enclosing the entire app in a React.StrictMode element.

root.render( 
  <React.StrictMode> 
    <App /> 
  </React.StrictMode> 
); 

Removing that <React.StrictMode> element will mean that none of the Strict Mode checks will occur. As such, the useEffect will only fire once, and the error won’t occur.

(In NextJS apps the same behaviour can be achieved by using reactStrictMode: false within nextjs.config.js.)

There is a significant disadvantage to disabling Strict Mode though – it was created to help you find bugs that already exist in your code, but which can be tricky to reliably reproduce in production. As such, Strict Mode lets you find and fix bugs before your users report them. If you disable it, then you have reduced your ability to find those bugs.

You can minimise the loss of bug-finding potential by using Strict Mode on individual parts of the component tree, but not on those parts that have issues due to the multiple firing of useEffect hooks.

Rewrite the Code so That the WebViewer Initialization Only Occurs Once.

If it isn’t possible to stop useEffect being called multiple times, then the function can be modified to only initialize the WebViewer on one of those occasions.

One way to do that is to create a new ref object that stores whether the WebViewer initialization code has been called before.

function App() { 
  const viewerDiv = useRef<HTMLDivElement>(null); 
  const beenInitialised = useRef<Boolean>(false); 
  useEffect(() => { 
    if (!beenInitialised.current) 
    { 
      beenInitialised.current = true; 
      WebViewer( 
      { 
        path: '/lib', 
        initialDoc: 'https://pdftron.s3.amazonaws.com/downloads/pl/webviewer-demo.pdf', 
        licenseKey: 'your_license_key'  // sign up to get a free trial key at https://dev.apryse.com 
      }, 
      viewerDiv.current as HTMLDivElement, 
    ).then( ); 
  } 
  }, []); 

  return ( 
    <div className="App"> 
      <div className='webViewer' ref={viewerDiv}/> 
    </div> 
   ); 
} 

With the code set like this, on the first time though the useEffect, the value for beenInitialised will be false, so the WebViewer will be created, and the beenInitialised ref will be set to true.

The second time that the useEffect fires, the value of beenInitialised will be true, so the WebViewer initialisation code will be skipped.

This is probably a better solution than disabling Strict Mode. However, the wisdom of creating additional code, solely to work around an issue that only appears in Development mode, is debatable due to the risk of introducing new bugs and additional future maintenance.

Summary

Copied to clipboard

The error “Two instances of WebViewer were created on the same HTML element” is certainly disconcerting. It has been caused by a change in behavior of the useEffect function when called in Development builds only and will not occur in Release builds.

There are a range of options available for working around the problem – which you choose is up to you. However, if you would like to discuss it with the Apryse team then please reach out to us on Discord.

Sanity Image

Roger Dunham

Share this post

email
linkedIn
twitter