GA4 page views on a single page app
React, Vue and other single page apps change URLs without page loads. How GA4 counts those views, the enhanced measurement setting, and how to avoid doubling.
In a single page app, clicking a link changes the URL and the view without loading a new page. The GA tag runs once on the first load, so unless something tells it about later changes, every visit looks like a single page view.
The built-in option
GA4's enhanced measurement has a Page views setting with an advanced option: page changes based on browser history events. It is on by default. With it on, GA4 sends a page_view whenever the app pushes a new URL to the browser history, which is what React Router, Vue Router, Next.js and most others do.
Find it in Admin, Data streams, your web stream, Enhanced measurement, then the settings for Page views.
The double counting trap
Many tutorials add a route-change listener that sends page_view. With the built-in option on, each navigation is counted twice. If your page views look inflated, search your code for page_view first.
Doing it yourself
The built-in option can fire before your app updates the page title, so some views carry the previous title. If that matters, turn the history option off and send the view after the route renders:
gtag("config", "G-XXXXXXX", { send_page_view: false });
router.afterEach(() => {
gtag("event", "page_view", {
page_location: location.href,
page_title: document.title,
});
});Strip what should not be recorded
Single page apps often keep tokens or keys in the query string. Remove them before they reach GA, as we did in keeping keys out of analytics.
Verify
Click through three routes with DebugView open and count the page_view events. There should be exactly three. GA4 DebugView shows how to turn it on.
GA4 + Search Console Setup installs the tag for eleven frameworks, including single page app setups, in the file where it runs once, so route changes are counted by GA4 and not twice by your code.
Questions
Does GA4 track route changes in a single page app?
Yes, when enhanced measurement's option for page changes based on browser history events is on. It listens for history changes and sends a page_view for each.
Why are my page views doubled?
Usually because enhanced measurement sends a page_view on each history change and your code also sends one. Keep one of them.
Should I send page views manually instead?
If you need control, such as waiting for the page title to update, turn off the history option and send page_view yourself with page_location and page_title. Otherwise the built-in option is simpler.