How to Implement Google Analytics in React: The Most Efficient Next.js Method for SPAs

The most efficient way to implement Google Analytics in a React single-page application is using Next.js's next/script component with the afterInteractive strategy, which loads the script without blocking rendering while deduplicating requests across client-side navigations.

Tracking user interactions in a React single-page application (SPA) requires careful handling of client-side route changes. The Next.js framework provides optimized primitives for loading third-party scripts, making it the ideal foundation for implementing Google Analytics React tracking without performance penalties.

Why next/script Is the Optimal Solution

The next/script component, implemented in packages/next/src/client/script.tsx, provides specific optimizations that make it superior to standard HTML <script> tags for Google Analytics React integration.

Automatic Script Deduplication

Unlike standard script tags that reload on every component mount, next/script maintains an internal cache of loaded URLs. When your SPA navigates client-side and the GoogleAnalytics component re-renders, the framework recognizes the existing GA library and prevents duplicate network requests.

Loading Strategy Optimization

The strategy prop controls when the script executes:

  • afterInteractive (recommended for GA): Loads after the page becomes interactive, ensuring critical rendering paths remain unblocked
  • lazyOnload: Defers loading until after all other resources have loaded, useful for lower-priority analytics

Implementing Google Analytics in Next.js

Step 1: Create the Google Analytics Component

Create a wrapper component that handles script loading and initialization. This pattern mirrors the canonical implementation in packages/third-parties/src/google/ga.tsx.

import { useEffect } from 'react';
import Script from 'next/script';
import { useRouter } from 'next/router';

declare global {
  interface Window {
    dataLayer: any[];
    gtag: (...args: any[]) => void;
  }
}

/**
 * Loads the GA script via next/script and tracks page views on route changes.
 */
export default function GoogleAnalytics({ measurementId }: { measurementId: string }) {
  const router = useRouter();

  // Initialise GA after the script is loaded
  const handleScriptLoad = () => {
    window.dataLayer = window.dataLayer || [];
    function gtag(...args: any[]) {
      window.dataLayer.push(args);
    }
    window.gtag = gtag;
    gtag('js', new Date());
    gtag('config', measurementId, {
      page_path: router.asPath,
    });
  };

  // Track subsequent client-side navigation
  useEffect(() => {
    const handleRouteChange = (url: string) => {
      window.gtag('event', 'page_view', { page_path: url });
    };
    router.events.on('routeChangeComplete', handleRouteChange);
    return () => {
      router.events.off('routeChangeComplete', handleRouteChange);
    };
  }, [router.events]);

  return (
    <>
      <Script
        src={`https://www.googletagmanager.com/gtag/js?id=${measurementId}`}
        strategy="afterInteractive"
        onLoad={handleScriptLoad}
      />
    </>
  );
}

Step 2: Register in Your Root Layout

For the Pages Router, import the component in pages/_app.tsx. For the App Router, use the root layout:

// app/layout.tsx
import GoogleAnalytics from '@/components/GoogleAnalytics';

export default function RootLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  return (
    <html lang="en">
      <body>
        <GoogleAnalytics measurementId="G-XXXXXXXXXX" />
        {children}
      </body>
    </html>
  );
}

Step 3: Handle Client-Side Route Changes

The useEffect in the component above subscribes to router.events on 'routeChangeComplete'. This captures every client-side navigation in your React SPA without reloading the GA script, ensuring continuous tracking across virtual page views.

Tracking Custom User Interactions

Once the script is loaded, you can track specific events throughout your application:

function TrackableButton() {
  const handleClick = () => {
    window.gtag('event', 'signup_button_click', {
      category: 'engagement',
      label: 'Signup CTA',
    });
  };

  return <button onClick={handleClick}>Sign up</button>;
}

Since the GA library is already initialized via next/script, these calls are fire-and-forget and impose virtually no overhead on your React application.

Key Source Files and Implementation Details

The Next.js repository contains several critical files that enforce and demonstrate these patterns:

  • packages/third-parties/src/google/ga.tsx: The canonical reference implementation for Google Analytics in Next.js, demonstrating proper next/script usage and gtag initialization.

  • packages/eslint-plugin-next/src/rules/next-script-for-ga.ts: An ESLint rule that warns developers if they attempt to load Google Analytics incorrectly, enforcing the use of next/script for optimal performance.

  • packages/next/src/client/script.tsx: The core implementation of the Script component, handling deduplication logic, loading strategies (afterInteractive, lazyOnload), and the internal script cache that prevents duplicate GA loads during SPA navigation.

  • examples/with-segment-analytics: Real-world examples demonstrating analytics integration patterns applicable to Google Analytics React implementations.

Summary

  • Use next/script with strategy="afterInteractive" to load Google Analytics without blocking the main thread or rendering path.
  • Initialize gtag in the onLoad callback to ensure the library is available before configuration.
  • Subscribe to router.events (routeChangeComplete) to track virtual page views during client-side navigation in your React SPA.
  • Leverage the built-in script deduplication in packages/next/src/client/script.tsx to prevent duplicate GA loads.
  • Follow the canonical implementation pattern in packages/third-parties/src/google/ga.tsx for production-ready code.

Frequently Asked Questions

Why not use a standard script tag or npm package for Google Analytics React integration?

Standard HTML script tags reload on every component mount in a React SPA, causing duplicate network requests and potential double-tracking. The next/script component in packages/next/src/client/script.tsx maintains an internal cache that deduplicates identical src values, ensuring the GA library loads exactly once regardless of how many times your component re-renders during client-side navigation.

How do I prevent duplicate page view events in a Next.js App Router application?

Initialize the gtag('config', ...) call only once inside the onLoad handler of your next/script component, passing the initial page_path from router.asPath. Then use useEffect to subscribe to router.events.on('routeChangeComplete', ...) for subsequent navigation events. This pattern ensures the initial configuration fires once while capturing every client-side route change as a distinct page view.

Does this implementation support Google Analytics 4 (GA4)?

Yes. The implementation described uses the standard gtag.js library loaded from https://www.googletagmanager.com/gtag/js, which supports both Universal Analytics and GA4. Simply use your GA4 Measurement ID (format G-XXXXXXXXXX) when calling gtag('config', measurementId). The canonical reference in packages/third-parties/src/google/ga.tsx follows this exact pattern for GA4 compatibility.

What loading strategy should I choose for performance-critical applications?

Use strategy="afterInteractive" (the default for external scripts) to load Google Analytics after the page becomes interactive, ensuring critical rendering paths remain unblocked. For even lower priority tracking where initial page views are less critical than core functionality, use strategy="lazyOnload" to defer loading until after all other resources have loaded, though this may miss early user interactions.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →