I want to load a script only on certain pages. Next.js remends 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.
import Script from "next/script";
const Comments = () => {
return (
<div className="giscus mt-16">
<Script
src="/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?
I want to load a script only on certain pages. Next.js remends 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.
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?
Share Improve this question asked Jan 18, 2022 at 15:48 GorvGoylGorvGoyl 49.4k36 gold badges259 silver badges262 bronze badges 2- Can you show us where you are using the Comments script? and also a little of your code structure could be helpful – lerichard_v Commented Jan 18, 2022 at 16:11
- @lerichard_v sure here's the code and here's the live site: gourav.io/blog/nextjs-cheatsheet – GorvGoyl Commented Jan 18, 2022 at 16:14
3 Answers
Reset to default 5I 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/ponents/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
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 ponent accepts the same props as next/script like onLoad and onReady. And, duplicate script tags aren't added to the body in case multiple ponents 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>
);
};
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>
)
}