How to track a Single Page Application (SPA)?
The SPA challenge for analytics
Single Page Applications (React, Vue, Angular, Next.js, Nuxt) do not reload the page during navigation. Content is dynamically updated via JavaScript and URL changes go through the History API (pushState, replaceState). However, traditional analytics tracking relies on the page load event to trigger a page_view. Without adaptation, GA4 sees only one page view per session, regardless of the user’s navigation.
GA4 with gtag.js partially handles this problem through enhanced measurement, which listens for History API events and sends a page_view on each URL change. However, this automatic detection can fail in certain cases: transitions that are too fast, URL updates after content rendering, or frameworks that use non-standard navigation mechanisms.
Implementation with GTM
For full control, disable enhanced measurement page_views in GA4 and manage them via GTM. Two approaches are possible. The first uses GTM’s native “History Change” trigger, which detects URL modifications via the History API. The second, more reliable, pushes an event into the data layer from the framework code on each route change (dataLayer.push({event: ‘virtual_page_view’, page_path: ‘/new-page’})).
In both cases, configure a GA4 Event tag of type page_view triggered by this event, with page_location and page_title parameters dynamically updated.
Common pitfalls and solutions
Timing is the main pitfall: the data layer push must occur after the title and URL have been updated, otherwise GA4 records the previous page’s values. In React, place the push in a useEffect after rendering. In Vue, use the router’s afterEach guard.
Another frequent problem: duplicate events. If enhanced measurement and your GTM implementation coexist, each navigation generates two page_views. Systematically disable enhanced measurement page views when using GTM for SPAs. Finally, test each route in GTM’s Preview mode to verify that variables are correctly transmitted.