Dan P
Dan P

Reputation: 63

NextJs <Link> doesn't rerun scripts

When I use the <Link> tag in NextJs to navigate between pages it doesn't rerun my scripts after changing pages. It only runs the scripts after the first page load or when I press reload. When using plain <a> tags instead, it works fine because the page reloads after each navigation. As far as I can tell this happens because the <Link> tag makes it a Single-Page Application and doesn't refresh the page when navigating between pages.

I would greatly appreciate anyway to have it rerun the scripts when navigating between pages or to have it refresh the page without using just plain <a> tags and losing the Single-Page Application functionality.

This code doesn't refresh the page

 <Link href="/page1">
   <a>Page 1</a>
 </Link>
 <Link href="/page2">
   <a>Page 2 </a>
 </Link>

This code does refresh the page

 <a href="/page1">Page 1</a>
 <a href="/page2">Page 2 </a>

I load in all my scripts using a scripts component

export default const MyScripts = () => {
    
    return (
        <Script
            strategy="afterInteractive"
            type="module"
            src="/scripts/myScript.js"
            onReady={() => {
                console.log("Ready")
            }}
        /> 
    )
}

One thing I've noticed is that the above onReady function fires each time I change pages. Maybe someone knows how to run the myScript.js from the onReady.

Upvotes: 2

Views: 2563

Answers (4)

ADTC
ADTC

Reputation: 10096

Caution: The hack of using history.pushState will not work reliably, especially when DOM elements need to be fetched using document.querySelector*. Anyway, it's also better to avoid hacks if you can.

The recommended way for Next.js is to use the Script component with the inline script.

Simply move the Javascript code from your script tag into a Script component. Remove the HTML script tag and any script file as you no longer need them.

import Script from 'next/script';

// Inside your exported component:
<Script id="myScript">
  // Paste the contents of /scripts/myScript.js here (and remove that file)
</Script>

Another clean way to do this is to use the useEffect React hook. In class-based components, you can use componentDidMount.

This idea was proposed here: samplep182's comment in: Normal scripts "" not working after page navigation · Issue #4477 · vercel/next.js

Simply move the Javascript code from your script tag into a useEffect block. Remove the script tag and any script file as you no longer need them.

import { useEffect } from 'react';

// Inside your exported component:
useEffect(() => {
  // Paste the contents of /scripts/myScript.js here (and remove that file)
}, []);

Notes:

  1. It's recommended to have an empty array [] as the dependency list, to execute this on page load only.

  2. For external scripts, please use the Script component with the src attribute.

Upvotes: 0

Richard Trembeck&#253;
Richard Trembeck&#253;

Reputation: 131

Even though it's not explicitly mentioned in the next/script docs, the scripts added via the <Script> component stay mounted on navigation. This is THE optimization that the component is supposed to do:

  • you don't define scripts in <head>, so the initial app load is faster
  • you postpone script loading to the time when you visit the page where the script is needed (potentially pre-loading it with the whole page when using <Link >). <script> is added to the body. The script is often third-party code adding properties to window.
  • when you navigate away, the script stays mounted in the body and window properties stay present
  • later when you come back to the page you can make use of the previously loaded/mounted script

When you need to run imperative code each time the page is visited/mounted, I'd go for useEffect. If you need to re-run some script as a whole on mount (not just re-use some of the work already done by it), you can:

  • access the script element and run the script again (from brief search, adding some URL param to script.src should work (script.src += '?x=x'))
  • not use next/script at all and try to go with classic <script> tag - it should get mounted/unmounted just like other elements

Upvotes: 0

Dan P
Dan P

Reputation: 63

I used to this to solve it. It rerun what you want to be rerun each time the URL changes.

function name(history){
    var pushState = history.pushState;
    history.pushState = function(state) {
       // YOUR CUSTOM HOOK / FUNCTION
       console.log('I am called from pushStateHook');
       return pushState.apply(history, arguments);
    };
})
name(history)

My implementation in myScript.js

function myFunctions(){
    //my code
}

function urlChange(history){
    let pushState = history.pushState;
    history.pushState = function(state) {
        myFunctions();//I insert my code here
        return pushState.apply(history, arguments);
    };
}
urlChange(history);

Also I'm pretty sure history.pushState = function(state){} is what runs each time the url changes not the urlChange() function.

Upvotes: 1

saguirrews
saguirrews

Reputation: 330

Could try to use the router object from the useRouter hook. Here's how you'd do that:

export const YourComponent = () => {
  const router = useRouter();
  ...
  return (<>
    <button onClick={() => router.push({ pathname: '/page1' })}/>...</button>
    // Or you can also use a div if you don't want it behaving like a button
    <div onClick={() => router.push({ pathname: '/page1' })}/>...
  ...

If this doesn't work, then I'd check for what the code for the Scripts component is doing and how it's reacting to state.

Let me know if that helps!

Upvotes: 0

Related Questions