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
Router.events.on callbacks in ./src/navigation.js that call console.log with the event that's been fired.<GtagScript/> from where it currently is in ./pages/document.jsNotice 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!
@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.)
Most helpful comment
Any updates on this? This simple addition is breaking all of my client-side routing on a static generated build: