Type something to search...
How to Track Single Page Applications in Google Analytics

How to Track Single Page Applications in Google Analytics

Single-page applications navigate between "pages" without ever triggering a full browser page load, which quietly breaks a basic assumption most analytics tags rely on — that a page_view event fires naturally every time the URL changes. Get this wrong, and your GA4 property looks like every visitor lands on your homepage and never goes anywhere else, no matter how much they actually navigate around your app. Fixing it just requires manually firing a page_view event on each route change.

Why the Default Tag Alone Isn't Enough

The standard gtag.js snippet fires an initial page_view when the script first loads. In a traditional multi-page site, that's sufficient, since every navigation is a full page load that re-runs the script. In a SPA, subsequent navigations are handled entirely by client-side routing (React Router, Vue Router, Next.js's router, etc.) — the script never reloads, so GA4 never hears about the "new page" unless you explicitly tell it.

Manually Firing Page Views on Route Change (React Example)

Using React Router, hook into route changes with useLocation and fire a page_view event on every change:

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

function usePageTracking() {
  const location = useLocation();

  useEffect(() => {
    gtag("event", "page_view", {
      page_path: location.pathname + location.search,
      page_title: document.title,
    });
  }, [location]);
}

export default usePageTracking;

Call usePageTracking() once near the top of your app (inside a component that's always rendered, like your root layout), and every client-side route change will now fire a proper page_view event with the correct path.

Next.js App Router Example

For Next.js specifically (App Router), route changes can be tracked with the usePathname and useSearchParams hooks inside a client component:

"use client";
import { usePathname, useSearchParams } from "next/navigation";
import { useEffect } from "react";

export default function Analytics() {
  const pathname = usePathname();
  const searchParams = useSearchParams();

  useEffect(() => {
    const url = pathname + (searchParams.toString() ? `?${searchParams}` : "");
    gtag("event", "page_view", {
      page_path: url,
      page_title: document.title,
    });
  }, [pathname, searchParams]);

  return null;
}

Mount this component once in your root layout, and it will fire a page_view on every route transition, including query string changes.

Vue Router Example

import { watch } from "vue";
import { useRoute } from "vue-router";

export function usePageTracking() {
  const route = useRoute();

  watch(
    () => route.fullPath,
    () => {
      gtag("event", "page_view", {
        page_path: route.fullPath,
        page_title: document.title,
      });
    }
  );
}

Implementing via Google Tag Manager

If you manage tags through GTM, listen for your router's own navigation events (most SPA routers dispatch a change event, or you can manually push to the dataLayer from your route change handler):

router.afterEach((to) => {
  window.dataLayer = window.dataLayer || [];
  dataLayer.push({
    event: "spa_page_view",
    page_path: to.fullPath,
    page_title: document.title,
  });
});

Then in GTM: create a Custom Event trigger listening for spa_page_view, and a GA4 Event tag with event name page_view, mapping page_location and page_title from the corresponding Data Layer Variables.

Preventing the Double Pageview on Initial Load

Since the base gtag.js snippet already fires one page_view automatically when it first loads, make sure your manual tracking doesn't duplicate that very first pageview. A common approach is disabling the automatic page view in your initial config and handling all page views — including the first — through your manual tracking hook:

gtag("config", "G-XXXXXXX", {
  send_page_view: false,
});

With send_page_view: false, the initial config call no longer fires its own page_view, and your route-tracking hook (which typically fires once on initial mount, and again on every subsequent route change) becomes the single source of truth for every pageview, including the first.

Verifying SPA Tracking

  1. Open Admin → DebugView.
  2. Navigate through several routes in your app without a full page reload.
  3. Confirm a distinct page_view event fires for each route, with the correct page_path and page_title values — not a stale value left over from the previous route.

A common bug here is a page_title that lags behind the actual route, since document.title may not update until slightly after your route-change effect fires — if that happens, delay the tracking call slightly (a setTimeout of a few milliseconds, or firing after the title-update logic explicitly completes) rather than reading a stale title.

Tracking Virtual "Screens" Beyond Simple Route Changes

Not every meaningful view change in a SPA corresponds to a URL change — a tabbed interface, a modal that reveals a distinct piece of content, or a multi-step wizard within a single route are all cases where the user is clearly viewing something new, but the browser's address bar never updates. For these, fire a manual page_view-style event (or a distinctly named custom event, like view_tab or wizard_step_view) at the moment of the meaningful visual change, using the same principle as route-based tracking but triggered by your component's own state changes rather than a router event:

function handleTabChange(tabName) {
  gtag("event", "page_view", {
    page_path: `${window.location.pathname}#${tabName}`,
    page_title: `${document.title} - ${tabName}`,
  });
  setActiveTab(tabName);
}

This keeps your engagement and funnel reporting accurate for interfaces that don't map cleanly onto traditional URL-based navigation.

Performance Considerations for SPA Tracking

Firing a tracking call on every route change is cheap, but it's worth being deliberate about not accidentally firing it multiple times per navigation — a common bug in React apps where an effect dependency array is set up incorrectly, causing the same page_view to fire two or three times for a single actual navigation. Test this specifically in DebugView by watching the event stream during a handful of real navigations, confirming exactly one page_view per route change rather than a burst of duplicates.

FAQ about Tracking Single Page Applications in Google Analytics

faq

Do I need a special GA4 setting for single-page applications?

No special property-level setting — you just need to manually fire page_view events on route changes, since GA4 doesn't automatically detect client-side navigation the way it detects a full page load.

Will Enhanced Measurement features like scroll tracking still work in a SPA?

Mostly, but they can behave inconsistently since they rely on standard page-load and DOM patterns that SPAs don't always trigger the same way — it's worth verifying each Enhanced Measurement feature specifically within your app's navigation flow.

How do I avoid counting the same page view twice on initial load?

Set send_page_view: false in your initial gtag config and handle all page views, including the first, through your manual route-tracking logic.

Does this apply to mobile apps built with frameworks like React Native?

The specific implementation differs (typically using the Firebase SDK's screen tracking rather than gtag.js), but the underlying principle is the same — screen changes in a mobile app framework need to be explicitly tracked, similar to route changes in a web SPA.

Why does my page_title sometimes show the wrong value after a route change?

This usually means the tracking call fires before document.title has actually updated for the new route — check your title-update logic's timing relative to when the tracking hook runs.

Can I use GA4's Enhanced Measurement outbound link tracking alongside manual SPA page tracking?

Yes — Enhanced Measurement's outbound click detection generally works fine in a SPA context since it's based on the link element itself, independent of your manual page view tracking for internal route changes.

Conclusion

Single-page applications need one deliberate fix to work correctly with Google Analytics: firing page_view manually on every client-side route change, since the browser never does it for you. Wire it into your router once, verify it in DebugView across a few real navigations, and your SPA's analytics will finally reflect how people actually move through your website.

Tags :
Share :

Related Posts

A Beginner's Guide to the GA4 Interface

A Beginner's Guide to the GA4 Interface

If you opened Google Analytics 4 for the first time and felt a little lost, you're not alone. GA4 looks nothing like the old Universal Analytics inte

Continue Reading
A Deep Dive into the Next Generation of Google Analytics

A Deep Dive into the Next Generation of Google Analytics

Google Analytics has long been a staple tool for countless businesses, enabling them to track, measure, and analyze data to gain insightful feedback

Continue Reading
A Practical Guide to Custom Events in Google Analytics 4

A Practical Guide to Custom Events in Google Analytics 4

GA4's automatic and Enhanced Measurement events cover a lot of ground, but sooner or later almost every site needs to track something specific to its

Continue Reading