Why does GA4 underreport pageviews on my React/Vue/Next app?
Single-page apps don't trigger fresh page loads on navigation — the URL changes via the History API but no full page-load event fires. GA4's automatic page tracking listens for full page-load events, so route changes are missed. The fix: manually fire a virtualPageView event on every route change, capturing the new URL, title, and referrer.
Each framework has its own pattern: Next.js App Router uses usePathname() + useSearchParams() in a layout component; React Router v6+ uses useLocation() listener; Vue Router uses navigation guards (router.afterEach); Angular uses Router events. The timing gotcha: route events fire BEFORE the new page renders, so reading from DOM at that moment returns stale or empty values.
Read URL/title from the route configuration, not from the DOM.
What goes wrong by default
GA4's Enhanced Measurement listens for browser page-load events to fire automatic page_view events. In a traditional multi-page site, every navigation triggers a page-load event, GA4 fires page_view, all is well.
In a single-page app:
- User clicks a link
- Framework intercepts the click
- URL changes via
history.pushState() - Framework swaps content in the DOM
- No page-load event fires — the page didn't actually reload
- GA4's Enhanced Measurement misses the navigation
- Only the initial app load is tracked as a page_view
Result: a 50-page SPA looks like a single page in GA4 reports. Sessions show 1 pageview per session even when users are clearly browsing extensively.
The fix: manual virtualPageView events
The pattern: detect route changes, manually push a page_view event with the new URL.
For GTM-based implementations:
In GTM, configure a trigger on virtualPageView event that fires the GA4 page_view tag.
For gtag-based implementations:
Either way, the key is firing on every route change. Implementation is framework-specific.
Framework-specific patterns
Next.js App Router (the 2026 default)
Create a Suspense-wrapped component that listens to pathname and search params:
Mount this component once in your root layout. It fires on every navigation.
React Router v6+
Call this hook in your top-level App component.
Vue Router
Note: read page_title from the route's meta config, not from document.title. The DOM title may not have updated yet when the navigation guard fires.
Angular Router
NavigationEnd fires after navigation completes. Earlier events (NavigationStart, ResolveStart) fire too soon.
The timing race condition
The trickiest SPA bug: route events fire BEFORE the new page renders. If your tracking code reads from document.title at that moment, you get the OLD title (the previous page's title), not the new one.
Want to see whether attribution loss is already distorting your channel data?
Symptoms:
- Page titles in GA4 reports shifted by one navigation
- New pages get titled with the previous page's title
- Stakeholder reports show "everyone visits the homepage" because the homepage's title is the most common cached value
Three fixes:
Fix 1 — Read from route configuration
Each framework has a way to access the route's metadata (title, page name) without reading from DOM:
- Next.js:
metadataAPI in page/layout files - React Router: route config or component props
- Vue Router:
route.meta.title - Angular:
route.data['title']
Use the framework's metadata, not document.title.
Fix 2 — Defer the push
Use setTimeout(..., 0) to push after the current event loop tick:
This delays the push until after React/Vue/Angular has updated the DOM. Less elegant than Fix 1 but simpler.
Fix 3 — Update title manually before pushing
In your route handlers, set document.title synchronously before pushing the event:
Works but requires discipline across the codebase.
The testing pattern
To validate SPA tracking before shipping:
Test 1 — Manual navigation in DevTools
Open DevTools → Network tab, filter for collect. Navigate through 5-10 routes in your SPA. Each navigation should produce one collect request with the correct page_path.
If you see no requests on navigation, route detection isn't working.
If you see multiple requests per navigation, you've got duplicate firing.
Test 2 — Tag Assistant verification
Install the Tag Assistant extension. Navigate through your SPA. Tag Assistant should show a virtualPageView event for each route change with correct parameters.
Test 3 — DebugView correlation
Open GA4 DebugView in a separate tab. Navigate your SPA. Each route change should produce a page_view event in DebugView within ~30 seconds with correct page_location, page_path, page_title.
Test 4 — Synthetic test
Use Playwright or Puppeteer to automate navigation through your app's main routes. Verify the resulting GA4 events match expected — automated regression testing for tracking.
FAQ: SPA Tracking in GA4: React, Vue, Next.js, and Angular Without the Pageview Bugs
What should a team validate first when spa tracking in ga4: 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 SPA Tracking in GA4: React, Vue, Next.js, and Angular Without the Pageview Bugs
ChatGPT, Atlas, Perplexity, Comet, Claude: How Each Shows Up in GA4 (2026 Reference)
In 2026, AI traffic in GA4 splits into three buckets. Browsers and assistants that pass clean referrers (Perplexity web, Perplexity Comet, Claude.ai, Copilot, Gemini standalone) appear with a recognisable source / medium like perplexity.ai / referral. Surfaces that strip the referrer (ChatGPT Atlas…
Perplexity Sources Report: How to Influence What It Cites in 2026
Perplexity citations correlate strongly with five factors: (1) ranking in Bing's top 10 for the underlying query (Perplexity uses Bing's index as fallback alongside its own ~5 billion-URL custom crawler), (2) a clear direct answer in the first 50 words of the relevant page…
Check SPA Tracking in GA4: React, Vue, Next.js, and Angular Without the Pageview Bugs before campaign reporting gets blamed for the wrong issue
Run a free GA4 audit to spot attribution breaks, UTM governance issues, self-referrals, and source/medium loss fast.