Next.js: Google Tag Manager breaks Next.js Link

Created on 15 Mar 2019  路  6Comments  路  Source: vercel/next.js

I'm using GTM for analytics on a Next.js site, and I've found that GTM breaks Next.js <Link> functionality.

Specifically, clicking on a <Link href={...}><a>text</a></Link> will do a full page refresh. I suspect GTM is doing something to the <a> tags in order to track user activity.

I've reproduced the issue here: https://codesandbox.io/s/q88owlwn7q

  • To check when Next.js dynamic navigation is working, I've added some Router.events.on callbacks in ./src/navigation.js that call console.log with the event that's been fired.
  • To either include GTM or not, add/remove <GtagScript/> from where it currently is in ./pages/document.js

Notice the following:

  • When GTM is excluded, Next.js works properly.

  • When GTM is included, dynamic navigation is broken (full refreshes happen instead).

For quick reference GTM is included via the following:

<script async
        src={`https://www.googletagmanager.com/gtm.js?id=${GA_TRACKING_ID}`}
    />

<script
        dangerouslySetInnerHTML={{
            __html: `
        window.dataLayer = window.dataLayer || [];
        function gtag(){dataLayer.push(arguments);}
        gtag('js', new Date());
        gtag('config', '${GA_TRACKING_ID}');
        `}}
</script>

(full example available in the CodeSandbox link)

Other discussions: Spectrum, SO.

Any way I could get around this? Thanks!

Most helpful comment

Any updates on this? This simple addition is breaking all of my client-side routing on a static generated build:

<script async src={`https://www.googletagmanager.com/gtm.js?id=${process.env.GTM_CONTAINER}`} />

All 6 comments

@dandrei The problem seems to be initializing these two lines during SSR:

gtag('js', new Date());
gtag('config', '${GA_TRACKING_ID}');

If you move those to _app.js like below it appears to be working fine:

componentDidMount() {
  gtag('js', new Date());
  gtag('config', GA_TRACKING_ID);
}

Thanks for the reply. Including GTM with SSR seems to be the issue.

The solution I've found, which also ensures the gtag calls run after the https://www.googletagmanager.com/gtm.js?id=... script is loaded is to include & initialize it from _app.js, and on the client side only.

So in my <GtagScript /> component (which I include in _app.js), I do this:

const [loaded, setLoaded] = useState(false);

useEffect(() => {
    setLoaded(true);
}, []);

return <>
    {loaded && <Head>
            <script
                id="gtm-js"
                async
                src={`https://www.googletagmanager.com/gtm.js?id=${GA_TRACKING_ID}`}
            />
            <script
                dangerouslySetInnerHTML={{
                    __html: `
        window.dataLayer = window.dataLayer || [];
        function gtag(){dataLayer.push(arguments);}
        gtag('js', new Date());
        gtag('config', '${GA_TRACKING_ID}');
        `
                }}/>
        </Head>}
</>

Closing the issue as I think I got to the bottom of it successfully.

@dandrei's comment seems more like a workaround than an actual solution. GTM is meant to be in the HTML response, and by implementing a useEffect or componentDidMount you defy that, right?

I found the problem to be with a certain event listener GTM adds to anchors, that gets priority over the Next.js Link callback.

It's concerning this code from gtm.js, I think:

function(g) {
  var h = g.target;
  if (h && 3 !== g.which && (!g.timeStamp || g.timeStamp !== d)) {
      d = g.timeStamp;
      h = Wa(h, ["a", "area"], 100);
      if (!h)
          return g.returnValue;
      var k = g.defaultPrevented || !1 === g.returnValue, l = Rf("lcl", k ? "nv.mwt" : "mwt", 0), m;
      m = k ? Rf("lcl", "nv.ids", []) : Rf("lcl", "ids", []);
      if (m.length) {
          var n = Nf(h, "gtm.linkClick", m);
          if (b(g, h, c) && !k && l && h.href) {
              var p = K((ai(h, "target") || "_self").substring(1))
                , t = !0;
              if (Rh(n, hf(function() {
                  t && p && (p.location.href = ai(h, "href"))
              }), l))
                  t = !1;
              else
                  return g.preventDefault && g.preventDefault(),
                  g.returnValue = !1
          } else
              Rh(n, function() {}, l || 2E3);
          return !0
      }
  }
};

When I remove that listener from the DOM, everything with Next.js works fine.

Any idea where this behaviour can be changed? Is it in Next.js or GTM?

Any updates on this? This simple addition is breaking all of my client-side routing on a static generated build:

<script async src={`https://www.googletagmanager.com/gtm.js?id=${process.env.GTM_CONTAINER}`} />

Was this ever actually resolved or just a bunch of workarounds?

This issue is inconsistent on our statically-generated site. It appears to depend on how quickly the gtm script loads: some pageloads are fine, but some have broken client-side routing and force a hard refresh when clicking links.

It looks like gtm.js is calling preventDefault() on link click events, and Next's Link component onClick handler doesn't do client-side routing if defaultPrevented is true:

onClick: (e: React.MouseEvent) => {
    if (child.props && typeof child.props.onClick === 'function') {
        child.props.onClick(e)
    }
    if (!e.defaultPrevented) {
        this.linkClicked(e)
    }
},

I followed @ijjk's workaround above, but I'm hoping there's a better solution to this. The inconsistency means that some devs might not experience this issue when testing, but some of their visitors will. (This is what happened to us - a user reported that the site seemed slower for them.)

Was this page helpful?
0 / 5 - 0 ratings

Related issues

jesselee34 picture jesselee34  路  3Comments

swrdfish picture swrdfish  路  3Comments

renatorib picture renatorib  路  3Comments

olifante picture olifante  路  3Comments

wagerfield picture wagerfield  路  3Comments