DEV Community

Cover image for Tracking Page Views in a React SPA with Google Analytics 4
Vicente G. Reyes
Vicente G. Reyes

Posted on • Originally published at vicentereyes.org

Tracking Page Views in a React SPA with Google Analytics 4

Adding Google Analytics (GA4) to a standard HTML website is straightforward: paste the tracking snippet into your <head> and you're done. Every time a user clicks a link, the browser fetches a new HTML page, and GA registers a page view.

But if you are building a Single Page Application (SPA) with React, Vite, and React Router, this out-of-the-box behavior breaks down.

In a React SPA, clicking a link doesn't trigger a page reload. React simply unmounts the old component and mounts the new one while manipulating the browser's URL history. Because the page never actually "reloads," Google Analytics never registers the new URL, and your analytics will show users seemingly stuck on the homepage forever.

Here is exactly how I solved this for my portfolio site.


1. The Environment Setup

First, avoid hardcoding your tracking ID into your source code. If your repo is public, anyone could scrape it. Add your Measurement ID (found in your GA dashboard, usually starting with G-XXXXXXXXXX) to your .env file.

VITE_GA_TRACKING_ID=G-**********
Enter fullscreen mode Exit fullscreen mode

2. The Initial HTML Snippet (Modified)

You still need the base Google Analytics tracking code in your index.html.

However, we need to make one critical modification: we must tell GA4 not to automatically track the initial page view. If we don't disable it, we will end up double-counting the first page load when our React Router listener kicks in later.

In index.html:

  <!-- Google tag (gtag.js) -->
  <script async src="https://www.googletagmanager.com/gtag/js?id=%VITE_GA_TRACKING_ID%"></script>
  <script>
    window.dataLayer = window.dataLayer || [];
    function gtag() { dataLayer.push(arguments); }
    gtag('js', new Date());

    // IMPORTANT: Disable the default page_view tracking here!
    gtag('config', '%VITE_GA_TRACKING_ID%', { send_page_view: false });
  </script>
Enter fullscreen mode Exit fullscreen mode

Note on Vite: I used %VITE_GA_TRACKING_ID% to inject the environment variable directly into the HTML at build time.

3. Creating the Route Listener Component

Now we need a way to listen to URL changes inside React and ping Google Analytics manually. We can create a lightweight, invisible component to handle this using the useLocation hook from react-router-dom.

Create components/Analytics.tsx:

import { useEffect } from 'react';
import { useLocation } from 'react-router-dom';

// Extend window object for TypeScript so it doesn't complain about window.gtag
declare global {
  interface Window {
    gtag: (...args: any[]) => void;
  }
}

export const Analytics = () => {
  const location = useLocation();

  useEffect(() => {
    // Read the tracking ID from environment variables
    const GA_MEASUREMENT_ID = import.meta.env.VITE_GA_TRACKING_ID;

    // Ensure the ID exists and the gtag function was loaded successfully
    if (GA_MEASUREMENT_ID && typeof window.gtag === 'function') {

      // Ping Google Analytics manually
      window.gtag('config', GA_MEASUREMENT_ID, {
        page_path: location.pathname + location.search,
      });

    }
  }, [location]); // Re-run this effect every time the URL changes

  return null; // This component is invisible
};
Enter fullscreen mode Exit fullscreen mode

4. Wiring it up to the Router

Finally, we need to mount our new <Analytics /> component.

The most important rule here is that Analytics must be placed inside the <BrowserRouter> but outside of the <Routes> block. If it's outside the Router, the useLocation hook will throw a contextual error.

In index.tsx:

import { Analytics } from './components/Analytics';
// ... other imports

const App = () => {
  return (
    <HelmetProvider>
      <BrowserRouter>

        {/* Place the listener here! */}
        <Analytics />
        <ScrollToTop />

        <Suspense fallback={<LoadingFallback />}>
          <Routes>
            <Route path="/" element={<HomePage />} />
            <Route path="/about" element={<AboutPage />} />
            {/* ... other routes */}
          </Routes>
        </Suspense>

      </BrowserRouter>
    </HelmetProvider>
  );
};
Enter fullscreen mode Exit fullscreen mode

The Result

And that's it!

Now, when a user lands on vicentereyes.org, the Google Analytics script loads.
Then, React boots up, mounts the router, hits the <Analytics /> component, and fires a page view event for /.

When the user clicks "Projects", React Router handles the transition, the URL updates to /projects, the useEffect inside <Analytics /> fires again, and a clean page view event for /projects is pushed to Google. Perfect SPA tracking, no heavy external libraries required.

Top comments (0)