17

Next.js v11 released a new Script component which has different strategies.

It is recommended to load Google TagManager with afterInteractive strategy.

I've tried

// _app.js

import Script from 'next/script';

class MyApp extends App {
  public render() {
    const { Component, pageProps } = this.props;

    return (
      <>
        <Script strategy="afterInteractive">
          {`(function(w,d,s,l,i){w[l]=w[l]||[];w[l].push({'gtm.start':` +
            `new Date().getTime(),event:'gtm.js'});var f=d.getElementsByTagName(s)[0],` +
            `j=d.createElement(s),dl=l!='dataLayer'?'&l='+l:'';j.async=true;j.src=` +
            `'https://www.googletagmanager.com/gtm.js?id='+i+dl;f.parentNode.insertBefore(j,f);` +
            `})(window,document,'script','dataLayer','GTM-XXXXXXX');`}
        </Script>
        <Component {...pageProps} />
      </>
    );
  }
}

export default MyApp;

It works fine, it loads google tag manager, but the problem is that it injects the same script on every page nav, which makes duplicate tags.

How to utilize the new Script component?

felixmosh
  • 32,615
  • 9
  • 69
  • 88

3 Answers3

17

You must set an id to your <Script> component because it has inline content (no src attribute).

Next can check if it has been already rendered or not and you will not have these duplications.

This is the eslint rule associated from Next :

next/script components with inline content require an id attribute to be defined to track and optimize the script.

See: https://nextjs.org/docs/messages/inline-script-id

So your solution could be :

    <Script id="gtm-script" strategy="afterInteractive">{`...`}</Script>

You should probably also install eslint for your next project :
Either run next lint or install eslint next config :

yarn add -D eslint eslint-config-next

and define the file .eslintrc.json with this content at least :

{
  "extends": ["next/core-web-vitals"]
}

Information about next.js eslint configuration.

Stephane L
  • 2,879
  • 1
  • 34
  • 44
  • from the [source code](https://github.com/vercel/next.js/blob/canary/packages/next/client/script.tsx#L42), it uses the id or the src (it make sense, since the src is unique enough). – felixmosh Sep 18 '21 at 09:23
  • 2
    Yes, this is specifically for inline scripts because in that case you don't have the `src` attribute. – Stephane L Sep 18 '21 at 12:23
  • But i don't have Script component without src, the script tag inside the document is the native script tag. – felixmosh Sep 19 '21 at 04:51
  • Yes you do have a `Script` component without a src attribute : ` – Stephane L Sep 19 '21 at 06:43
  • Ha, but this is my initial trail (which not worked at all), the solution is my answer, not my question :] – felixmosh Sep 19 '21 at 07:52
  • 1
    Yes I think that your answer is a solution, and adding an id is another solution to your initial problem (if you want to use next/script). I am not sure, but maybe the advantage of next/script would be that it minifies the content of the script (but I didn't check). – Stephane L Sep 19 '21 at 14:20
  • The requirement of id is applies **only** to inline `Script` tags, which I don't have in my example. – felixmosh Aug 10 '22 at 07:27
9

My final solution was to break apart the GTM script.

Putting the initial dataLayer object on the window in _document page.

// _document.js

import Document, { Head, Html, Main, NextScript } from 'next/document';

export default class MyDocument extends Document {
  render() {
    return (
      <Html lang="en">
        <Head>
          <meta charSet="utf-8" />

          <script
            dangerouslySetInnerHTML={{
              __html:
                `(function(w,l){` +
                `w[l] = w[l] || [];w[l].push({'gtm.start':new Date().getTime(),event:'gtm.js'});` +
                `})(window,'dataLayer');`,
            }}
          />
        </Head>
        <body>
          <Main />
          <NextScript />
        </body>
      </Html>
    );
  }
}

Loading the GMT script with Script component (which is not allowed to be used in the _document page)

// _app.js

import Script from 'next/script';

class MyApp extends App {
  public render() {
    const { Component, pageProps } = this.props;

    return (
      <>
        <Script src={`https://www.googletagmanager.com/gtm.js?id=GMT-XXXXXXX`} />
        <Component {...pageProps} />
      </>
    );
  }
}

export default MyApp;
felixmosh
  • 32,615
  • 9
  • 69
  • 88
2

Your inline scripts require an "id" parameter, so that Next can internally check and avoid loading the scripts again.

The documentation mentioned it but they missed this in the first release.

It was later added as a patch so upgrading your Next would solve this issue

Patch - https://github.com/vercel/next.js/pull/28779/files/11cdc1d28e76c78a140d9abd2e2fb10fc2030b82

Discussion Thread - https://github.com/vercel/next.js/pull/28779

Mudit Jaju
  • 590
  • 7
  • 17