javascriptreactjsnext.js

Nextjs loading Third Party Scripts which add to window load event


I see this as common practice with a lot of third party scripts where they have an initialization function that gets attached to window load event so that on page load, the script does what it needs to do. This doesn't seem to work well in an SPA. Let me explain.

Lets say you have a client component that uses the Script component, then the page is loaded and the component is mounted, the script is added, it adds its function to window load event, load event eventually fires, then the components target element gets modified as intended.

But here's the issue, if you were to navigate away, that's not considered a new page load, that's just a route change. So if I were to leave that page and go back using Link's for example, once I return to that page with the component loading the script, the script doesn't actually do anything anymore. The load event has already fired at the beginning of the application lifecycle so the script wouldn't initialize when the component is mounted again.

This is not an edge case with the script. It is common practice for scripts to leverage window load for initialization. Load event doesn't fire more than once in the lifecycle of the session in an SPA.

What's the intended way to do this in Next.js?

Here is an example component.

'use client';
import Script from 'next/script';

function ExampleComponent() {
  return (
    <>
      <Script src="example_script.js" />
      <div id="exampleContainer"></div>
    </>
  )
}

Here is an example of what could be the contents of the example script

// a bunch of minified code that doesn't matter for the question
...
window.addEventListener("load", scriptsInitFunction);

Solution

  • You should not rely on window.load in SPAs like Next.js because it only happens once. You should manually call the constructor after the script is loaded or when the component is remounted.

    If the script does not expose the constructor explicitly, you need to double check its logic to ensure it can be safely re-initialized.

    There are 2 correct ways to handle this in Next.js that I knew:

    1. Auto-restart script when component mounts

    Let's say your script has the following snippet:

    window.scriptsInitFunction = function () {
      // DOM manipulation, etc.
    };
    window.addEventListener("load", window.scriptsInitFunction);
    

    And you can call scriptsInitFunction again when the component is mounted like this:

    'use client';
    import Script from 'next/script';
    import { useEffect } from 'react';
    
    function ExampleComponent() {
      useEffect(() => {
        // Wait for the script to load before calling.
        if (window.scriptsInitFunction) {
          window.scriptsInitFunction();
        }
      }, []);
    
      return (
        <>
          <Script
            src="/example_script.js"
            // Make sure the script is loaded after hydration is complete
            strategy="afterInteractive" 
            onLoad={() => {
              // Call again if you want to make sure the script always runs
              if (window.scriptsInitFunction) {
                window.scriptsInitFunction();
              }
            }}
          />
          <div id="exampleContainer"></div>
        </>
      );
    }
    

    This way don't depend on window.load, but call the constructor directly when the component is mounted.

    1. Load the script manually and initialize later
    'use client';
    import { useEffect } from 'react';
    
    function ExampleComponent() {
      useEffect(() => {
        const script = document.createElement('script');
        script.src = '/example_script.js';
        script.async = true;
        script.onload = () => {
          if (window.scriptsInitFunction) {
            window.scriptsInitFunction();
          }
        };
        document.body.appendChild(script);
    
        return () => {
          // Clean up if necessary
        };
      }, []);
    
      return <div id="exampleContainer"></div>;
    }
    

    If the script doesn't provide an explicit global init function, you can load the script with document.createElement('script'), then attach it to the DOM. Once loaded, handle DOM initialization.