Single page applications break Google Tag Manager in ways that are not obvious until you audit your data and find that 60% of your page views are missing, events are firing on the wrong pages, or user journeys show impossible navigation patterns. SPAs require a fundamentally different dataLayer architecture than traditional multi-page websites.
Why SPAs Break Traditional GTM Tracking
Traditional GTM triggers are built around browser events: the page loads, GTM fires, triggers evaluate, tags run. In a SPA, navigation updates the URL and DOM without a page reload. GTM fires once on the initial page load and then sees a static environment for all subsequent navigations. GTM’s page-based variables become stale immediately after the first navigation, and a Page View trigger set to fire on All Pages fires only on the initial load, missing every subsequent route change.
The Core Solution: History Change Trigger
GTM has a built-in History Change trigger type that fires when the browser’s History API is used to navigate (pushState or replaceState). SPAs that use a history-based router will trigger this on every route change. Create a History Change trigger in GTM, then create a GA4 Event tag with event name page_view using this trigger. Set the page_location parameter to the Page URL built-in variable — by the time a History Change fires, GTM’s built-in variables have already updated to reflect the new URL.
Pushing Route Data to the dataLayer
In React with React Router v6, add a useEffect hook that fires on route change:
import { useLocation } from 'react-router-dom';
import { useEffect } from 'react';
function RouteChangeTracker() {
const location = useLocation();
useEffect(() => {
window.dataLayer = window.dataLayer || [];
window.dataLayer.push({
event: 'virtual_page_view',
page_path: location.pathname,
page_location: window.location.href
});
}, [location]);
return null;
}

Vue and Angular Implementations
In Vue 3 with Vue Router, use the router.afterEach hook to push virtual_page_view events. In Angular, inject the Router service and subscribe to NavigationEnd events in your AppComponent. In all frameworks, the pattern is the same: hook into the framework’s navigation lifecycle, push a dataLayer event with the new page context, and trigger GA4 page_view tags from that custom event rather than from GTM’s built-in Page View trigger.
The dataLayer Reset Problem
In traditional multi-page sites, the dataLayer resets on each page load. In SPAs, the dataLayer persists across all route changes. Push a reset object at the start of each virtual page view, clearing stale product_id, product_name, and product_category variables set by previous routes. Use undefined (not null or empty string) to properly clear GTM variables.
Testing SPA Tracking in GTM Preview
GTM’s Preview mode works with SPAs but requires care. Always navigate using the SPA’s own internal links rather than the browser’s address bar — using the address bar causes a full page reload which reconnects Preview mode but is not the SPA navigation path you want to test. Each route change should appear as a new message in the Preview panel. Verify that the GA4 page_view tag fires on each navigation, that page_location reflects the new URL, and that stale variables from the previous route have been cleared.
