GorvGoyl
GorvGoyl

Reputation: 49480

How to unmount next/script on page change in next.js?

I want to load a script only on certain pages. Next.js recommends using next/script tag for it. However, when I navigate to some different pages I can still see the script present at the end of body in HTML.

enter image description here

import Script from "next/script";

const Comments = () => {
  return (
    <div className="giscus mt-16">
      <Script
        src="https://giscus.app/client.js"
        data-repo="GorvGoyl/Personal-Site-Gourav.io"
        data-repo-id="MDEwOlJlcG9zaXRvcnkyOTAyNjQ4MTU="
        data-category="Announcements"
        data-category-id="DIC_kwDOEU0W784CAvcn"
        data-mapping="pathname"
        data-reactions-enabled="0"
        data-emit-metadata="0"
        data-theme="light"
        data-lang="en"
        crossOrigin="anonymous"
        strategy="lazyOnload"
        onError={(e) => {
          console.error("giscus script failed to load", e);
        }}
      ></Script>
    </div>
  );
};

I suspect Next.js is not cleaning up the script on route change action. How do I make sure that scripts get removed on page change?

Upvotes: 15

Views: 11158

Answers (3)

Davey
Davey

Reputation: 2514

This is an extension of @Chaitanya Kulkarni 's answer. It is different because it leverages next/script.

Benefits of using next/script are that ScriptWithCleanup component accepts the same props as next/script like onLoad and onReady. And, duplicate script tags aren't added to the body in case multiple components with the same script tag are rendered on the page.

Custom Component

import { useEffect } from 'react';
import { useRouter } from 'next/router';
import Script, { ScriptProps } from 'next/script';

export const ScriptWithCleanup = (props: ScriptProps) => {
  const router = useRouter();

  useEffect(() => {
    // Needed for cleaning residue left by the external script
    // that can only be removed by reloading the page
    const onRouterChange = (newPath: string) => {
      window.location.href = router.basePath + newPath;
    };
    router.events.on('routeChangeStart', onRouterChange);

    return () => {
      router.events.off('routeChangeStart', onRouterChange);
    };
  }, [router, props]);

  return <Script {...props} />;
};

Usage

import { ScriptWithCleanup } from './ScriptWithCleanup';

export const YourComponent = () => {
  return (
    <div>
      <ScriptWithCleanup
        src="/js/theScript.js"
        onLoad={() => console.log('Stripe script loaded')}
      />
    </div>
  );
};

Upvotes: 2

Chaitanya Kulkarni
Chaitanya Kulkarni

Reputation: 74

I was facing the same problem. Only solution that works for me is to reload the page as the routeChangeStart. I also made it in a custom Hook so can be reused with ease.

Custom Hook

import { useRouter } from "next/router";
import { useEffect } from "react";

export default function useScript(url: string) {
    const router = useRouter();
    useEffect(() => {
        const script = document.createElement("script");
        script.src = url;
        script.async = true;
        document.body.appendChild(script);

        // Needed for cleaning residue left by the external script that can only be removed by reloading the page
        const onRouterChange = (newPath: string) => {
            window.location.href = router.basePath + newPath;
        };
        router.events.on("routeChangeStart", onRouterChange);

        return () => {
            router.events.off("routeChangeStart", onRouterChange);

            document.body.removeChild(script);
        };
    }, [router, url]);
}

Explaination:

  • This hook creates new script element, sets its url to provided url and appends it to body
  • Importing script is easy the hard part is to remove it and all the residue it leaves behind mailny on window object, the hacky and easy way is to just reload the page
  • So we are adding event listener for route change and then we are redirecting to that page so the whole page refreshes and all the previous js script get unloaded

Usage:

Just need to call useScript with url of javascript

import React from "react";
import NavBar from "../src/components/NavBar";
import useScript from "../src/hooks/useScript";

type Props = {};

const ScriptLoader = (props: Props) => {
    useScript("/js/theScript.js");
    return (
        <>
            <NavBar />

            <div className="container">This is normal Page</div>
        </>
    );
};

export default ScriptLoader;

Note:

If you want to access property set on window object from the script its best to add setTimeout to check if that object is present or not as the loading of script might take some time, after that you can set some flag to true

Upvotes: 5

nicholas_juno2231
nicholas_juno2231

Reputation: 9

I did a lot of crawling on the internet and tried a lot of different methods from forums because this seems to be an issue with next.js but the one that worked for me with the least amount of code was adding a script tag with javascript in the useEffect hook. I tried it randomly and I didn't think it would work to unmount it on it's own but it did without any extras. I have the brackets empty so it only runs on mount.

export default function Page() {
      
      useEffect(() => {
            const contentDiv = document.getElementById("content");
            const newScript = document.createElement("script");
            newScript.src = "url";
            newScript.id = "script-id";
            newScript.async = true;
            // add rest of your script items
            contentDiv?.appendChild(newScript);   
        }, [])
    
       return (
           <div className='content'>
  
           </div>
      )
}

Upvotes: 0

Related Questions