nextjs-google-analytics icon indicating copy to clipboard operation
nextjs-google-analytics copied to clipboard

Support for server components in Next 13

Open EvHaus opened this issue 3 years ago • 9 comments

Thanks for the excellent library. I noticed that in Next 13, the library isn't supported with server components. If you want to use the GoogleAnalytics component, you must set that component to "use client". Since in most cases I want <GoogleAnalytics /> to be set at the root in (app/layout.tsx) it means that I need the set the top-level component of my app to be a client component, defeating all the benefits of Next 13's server components.

If I try to use <GoogleAnalytics /> in a server component I get this error:

error - (sc_server)/node_modules/next/dist/shared/lib/router-context.js (8:37) @ eval
error - TypeError: _react.default.createContext is not a function
    at eval (webpack-internal:///(sc_server)/./node_modules/next/dist/shared/lib/router-context.js:8:38)
    at Object.(sc_server)/./node_modules/next/dist/shared/lib/router-context.js (/home/myproject/.next/server/app/page.js:880:1)
    at __webpack_require__ (/home/myproject/.next/server/webpack-runtime.js:33:43)
    at eval (webpack-internal:///(sc_server)/./node_modules/next/dist/client/router.js:24:22)
    at Object.(sc_server)/./node_modules/next/dist/client/router.js (/home/myproject/.next/server/app/page.js:671:1)
    at __webpack_require__ (/home/myproject/.next/server/webpack-runtime.js:33:43)
    at eval (webpack-internal:///(sc_server)/./node_modules/next/router.js:2:18)
    at Object.(sc_server)/./node_modules/next/router.js (/home/myproject/.next/server/app/page.js:1177:1)
    at __webpack_require__ (/home/myproject/.next/server/webpack-runtime.js:33:43)
    at eval (webpack-internal:///(sc_server)/./node_modules/nextjs-google-analytics/dist/hooks/usePageViews.js:7:18) {
  type: 'TypeError',
  page: '/'
}

I suspect it's because of the useEffect call in usePageViews.

The next-auth team was also recently struggling with a similar issue I believe (see comment).

I'm curious of there is any way to make this library work in a server component (ie. not use any useEffect calls)?

EvHaus avatar Nov 05 '22 02:11 EvHaus

Thanks for the excellent library. I noticed that in Next 13, the library isn't supported with server components. If you want to use the GoogleAnalytics component, you must set that component to "use client". Since in most cases I want <GoogleAnalytics /> to be set at the root in (app/layout.tsx) it means that I need the set the top-level component of my app to be a client component, defeating all the benefits of Next 13's server components.

If I try to use <GoogleAnalytics /> in a server component I get this error:

error - (sc_server)/node_modules/next/dist/shared/lib/router-context.js (8:37) @ eval
error - TypeError: _react.default.createContext is not a function
    at eval (webpack-internal:///(sc_server)/./node_modules/next/dist/shared/lib/router-context.js:8:38)
    at Object.(sc_server)/./node_modules/next/dist/shared/lib/router-context.js (/home/myproject/.next/server/app/page.js:880:1)
    at __webpack_require__ (/home/myproject/.next/server/webpack-runtime.js:33:43)
    at eval (webpack-internal:///(sc_server)/./node_modules/next/dist/client/router.js:24:22)
    at Object.(sc_server)/./node_modules/next/dist/client/router.js (/home/myproject/.next/server/app/page.js:671:1)
    at __webpack_require__ (/home/myproject/.next/server/webpack-runtime.js:33:43)
    at eval (webpack-internal:///(sc_server)/./node_modules/next/router.js:2:18)
    at Object.(sc_server)/./node_modules/next/router.js (/home/myproject/.next/server/app/page.js:1177:1)
    at __webpack_require__ (/home/myproject/.next/server/webpack-runtime.js:33:43)
    at eval (webpack-internal:///(sc_server)/./node_modules/nextjs-google-analytics/dist/hooks/usePageViews.js:7:18) {
  type: 'TypeError',
  page: '/'
}

I suspect it's because of the useEffect call in usePageViews.

The next-auth team was also recently struggling with a similar issue I believe (see comment).

I'm curious of there is any way to make this library work in a server component (ie. not use any useEffect calls)?

You may want to create a custom component with “use client” flag and return <GoogleAnalytics /> to solve this problem until the package become compatible to Next.js 13. It is a official method provided in the beta documentation.

See https://beta.nextjs.org/docs/rendering/server-and-client-components#third-party-packages

webbipage avatar Nov 25 '22 13:11 webbipage

Another problem with the /app folder is the lack of router events in next/navigation: https://github.com/vercel/next.js/issues/45499. This may get fixed by https://github.com/vercel/next.js/pull/46391.

kachkaev avatar Feb 26 '23 17:02 kachkaev

In addition to the above you'd also need to account for how ga wants to track server side apps:

  const gtmDomain = process.env.NEXT_PUBLIC_GTM_DOMAIN;


  gtag('config', '${gaMeasurementId}', {
     page_path: window.location.pathname,
     transport_url: 'https://${gtmDomain}',
     first_party_collection: true
  });

https://github.com/EmilianoGarciaLopez/nextjs-google-analytics-GTM/blob/main/src/GoogleAnalytics.tsx#L26-L27

airtonix avatar Mar 15 '23 10:03 airtonix

Thanks for the info. I will try your suggestions.

LZL0 avatar May 14 '23 15:05 LZL0

Based on the info above, I got my answer and it worked for me. Here is my solution:

  1. create a custom component with “use client” flag.

// AnalyticsProvider component
'use client';

import React from 'react';

import Script from 'next/script';

type Props = { children: React.ReactNode };

export default function AnalyticsProvider(children: Props) {
  // if you use env file
  // const gaMeasurementId = process.env.NEXT_PUBLIC_GA_MEASUREMENT_ID;
  // const domain = process.env.NEXT_PUBLIC_GTM_DOMAIN;
  // const reportDomain = process.env.NEXT_PUBLIC_GTM_REPORT_DOMAIN;

  // or just hard code
  const gaMeasurementId = 'your_measurement_id';
  const domain = 'https://www.googletagmanager.com';
  const reportDomain = 'https://www.google-analytics.com';

  return (
    <>
      {children}
  {/* I tried importing the Google Analytics component from the nextjs-google-analytics package, but it didn't work.
So I chose to write the script manually. */}
        {/* <GoogleAnalytics trackPageViews /> */}
      <Script
        id="google-analytics-js-cdn"
        src={`${domain}/gtag/js?id=${gaMeasurementId}`}
        strategy="afterInteractive"
      />
      <Script
        id="google-analytics"
        strategy="afterInteractive"
        dangerouslySetInnerHTML={{
          __html: `
              window.dataLayer = window.dataLayer || [];
              function gtag(){dataLayer.push(arguments);}
              gtag('js', new Date());
            
              gtag('config', '${gaMeasurementId}', {
                 page_path: window.location.pathname,
                 transport_url: '${reportDomain}',
                 first_party_collection: true
              });
          `,
        }}
      />
    </>
  );
}
  1. Wrap your html elements with the AnalyticsProvider component in app/layout.tsx
// app/layout.tsx

// import the AnalyticsProvider you just created
import AnalyticsProvider from 'xxxx';

type Props = {
  children: React.ReactNode;
};

export default async function RootLayout({ children }: Props) {
  return (
    <html lang="en">
      <body>
        <AnalyticsProvider>
          {/* your html element */}
          {children}
        </AnalyticsProvider>
      </body>
    </html>
  );
}

aifuxi avatar Jun 27 '23 16:06 aifuxi

Based on the info above, I got my answer and it worked for me. Here is my solution:

  1. create a custom component with “use client” flag.
// AnalyticsProvider component
'use client';

import React from 'react';

import Script from 'next/script';

type Props = { children: React.ReactNode };

export default function AnalyticsProvider(children: Props) {
  // if you use env file
  // const gaMeasurementId = process.env.NEXT_PUBLIC_GA_MEASUREMENT_ID;
  // const domain = process.env.NEXT_PUBLIC_GTM_DOMAIN;
  // const reportDomain = process.env.NEXT_PUBLIC_GTM_REPORT_DOMAIN;

  // or just hard code
  const gaMeasurementId = 'your_measurement_id';
  const domain = 'https://www.googletagmanager.com';
  const reportDomain = 'https://www.google-analytics.com';

  return (
    <>
      {children}
  {/* I tried importing the Google Analytics component from the nextjs-google-analytics package, but it didn't work.
So I chose to write the script manually. */}
        {/* <GoogleAnalytics trackPageViews /> */}
      <Script
        id="google-analytics-js-cdn"
        src={`${domain}/gtag/js?id=${gaMeasurementId}`}
        strategy="afterInteractive"
      />
      <Script
        id="google-analytics"
        strategy="afterInteractive"
        dangerouslySetInnerHTML={{
          __html: `
              window.dataLayer = window.dataLayer || [];
              function gtag(){dataLayer.push(arguments);}
              gtag('js', new Date());
            
              gtag('config', '${gaMeasurementId}', {
                 page_path: window.location.pathname,
                 transport_url: '${reportDomain}',
                 first_party_collection: true
              });
          `,
        }}
      />
    </>
  );
}
  1. Wrap your html elements with the AnalyticsProvider component in app/layout.tsx
// app/layout.tsx

// import the AnalyticsProvider you just created
import AnalyticsProvider from 'xxxx';

type Props = {
  children: React.ReactNode;
};

export default async function RootLayout({ children }: Props) {
  return (
    <html lang="en">
      <body>
        <AnalyticsProvider>
          {/* your html element */}
          {children}
        </AnalyticsProvider>
      </body>
    </html>
  );
}

@aifuxi snippet worked for me

marleymwangi avatar Jul 07 '23 09:07 marleymwangi

Forgive me for being a bit blonde, but does the above snippet mean it's a bit redundant to use this package for the time being when using server components in Next.js 13

aamir-madari avatar Jul 17 '23 20:07 aamir-madari

You can create a client component that wraps the package component. This can be used in app/layout

"use client";
import { GoogleAnalytics } from "nextjs-google-analytics";

export default function Analytics({ measurementId }) {
  return <GoogleAnalytics gaMeasurementId={measurementId} trackPageViews />;
}

anthonyholmes avatar Aug 24 '23 03:08 anthonyholmes

Create a client component in your providers/components dir. Then, use the component in your layout file.

  • GoogleAnalytics.tsx
"use client";

import type { NextWebVitalsMetric } from "next/app";
import { event, GoogleAnalytics as GAnalytics } from "nextjs-google-analytics";

// Send NextJS Web Vitals to GA
export function reportWebVitals({
  id,
  name,
  label,
  value,
}: NextWebVitalsMetric) {
  event(name, {
    category: label === "web-vital" ? "Web Vitals" : "Next.js custom metric",
    value: Math.round(name === "CLS" ? value * 1000 : value), // values must be integers
    label: id, // id unique to current page load
    nonInteraction: true, // avoids affecting bounce rate.
  });
}

export default function GoogleAnalytics() {
  return <GAnalytics trackPageViews />;
}

  • app/layout.tsx
import { ReactNode } from "react";
import Header from "@/ui/header/header";
import Footer from "@/ui/footer/footer";
import GoogleAnalytics from "@/providers/google-analytics";


interface RootLayoutProps {
  children: ReactNode;
}

export default function RootLayout({ children }: RootLayoutProps) {
  return (
    <html lang="en">
      <GoogleAnalytics />
      <body>
        <Header />
        <div>{children}</div>
        <Footer />
      </body>
    </html>
  );
}


alipiry avatar Dec 13 '23 14:12 alipiry