Why SPAs break standard GA4 tracking
In a traditional multi-page website, every page navigation triggers a full page load. GA4's enhanced measurement fires a page_view event on each load automatically. In a single-page application (React, Vue, Next.js, Angular), navigating between routes does NOT reload the page — JavaScript updates the DOM and the URL without triggering a full page load. Result: GA4 fires one `page_view` event on the initial load, then nothing. Users who navigate through 10 SPA routes appear in GA4 as a 1-page session.
Engagement rate is artificially low (many sessions appear as <10 seconds because only one page_view fires). All multi-page conversion funnels break.
Approach 1 — GA4 Enhanced Measurement History Change (easiest, least reliable)
GA4's enhanced measurement includes a History Changes option that fires page_view when the browser's History API changes (pushState/replaceState).
Enable: Admin → Data Streams → Web → Enhanced Measurement → History changes (enabled by default)
When this works: React Router, Vue Router, Angular Router — all use the History API for navigation. History Change enhanced measurement should fire page_view on each route change.
When this breaks:
- Hash-based routing — if your SPA uses
#/pathrouting (hash fragments), the History API isn't used and the History Change trigger doesn't fire - Modals that update the URL — if opening a modal changes the URL (e.g.,
/products#modal-123), GA4 fires a spuriouspage_viewfor the modal - React Router with multiple history updates per navigation — some implementations trigger multiple History API calls per route change, causing double-firing of
page_view
Check for double-firing: Open GA4 DebugView and navigate between pages. If each navigation produces 2 page_view events, double-firing is occurring.
Approach 2 — GTM History Change Trigger (reliable for most SPAs)
GTM has a built-in trigger type for SPA route changes: History Change.
GTM configuration:
- Triggers → + New → Trigger Type → History Change
- This trigger fires: when browser history changes (same as GA4 enhanced measurement History Change, but manageable in GTM)
- Create a GA4 Event tag triggered by History Change → Event Name:
page_view→ parameters:page_location={{Page URL}},page_title={{Page Title}} - Set firing priority higher than other page_view tags to prevent race conditions
Disable enhanced measurement History Changes if using GTM History Change trigger — having both active causes double page_view events.
Want to see which hidden implementation gaps are affecting your GA4 data quality?
React Router consideration: React Router v6+ uses the History API correctly and GTM's History Change trigger fires reliably. No additional configuration needed for standard React Router setups.
Known issue — Next.js App Router:
Next.js 13+ App Router uses React Server Components and a custom router that doesn't always fire standard History API events. The GTM History Change trigger may miss some navigations. See framework-specific section below.
Approach 3 — Manual virtual pageview push (most reliable)
For full control, push page_view events to the dataLayer manually from within your SPA's router/navigation logic.
React Router v6
In GTM: Create a Custom Event trigger for page_view. Create a GA4 Event tag: event name page_view, parameters from the dataLayer variables. Disable enhanced measurement page_view to prevent double-firing.
Next.js 13+ App Router
Note: The Suspense wrapper is required for useSearchParams() in Next.js 13+ — without it, the build fails.
Vue Router
Angular
The page title timing problem
All SPA tracking approaches face the same issue: when the route changes, the new page's <title> may not yet be set when the page_view event fires (especially in React/Next.js where title updates are asynchronous).
Fix — small setTimeout for title capture:
FAQ: GA4 SPA Tracking: React, Vue, Next.js, and Angular Without the Pageview Bugs
What should a team validate first when ga4 spa tracking: react, vue, next.js, and angular without the pageview bugs appears?
How do I know whether the fix actually worked?
When should this become a full GA4 audit instead of a quick fix?
Related guides for GA4 SPA Tracking: React, Vue, Next.js, and Angular Without the Pageview Bugs
BigQuery Cost Optimisation for GA4 Exports: 9 SQL Patterns (2026)
The biggest cost wins come from nine SQL patterns: (1) partition pruning via _TABLE_SUFFIX BETWEEN (10–50x cost difference vs derived filters), (2) clustering on source/medium/event_name (30–60% reduction on top of partitioning), (3) explicit column selection (never SELECT *)…
How to Stitch GA4 BigQuery Sessions Manually (2026)
GA4 doesn't store sessions as records in BigQuery exports — only individual events with session identifiers. To reconstruct sessions: join on user_pseudo_id + (SELECT value.int_value FROM UNNEST(event_params) WHERE key='ga_session_id') as the unique session key…
Run a GA4 audit before ga4 spa tracking: react, vue, next.js, and angular without the pageview bugs spreads into reporting decisions
Use GA4 Audits to surface implementation gaps, broken signals, and the next fixes to prioritize before the issue becomes harder to trust or explain.