Skip to content
← All articles

Analytics & Tracking

How to Track Page Views in a Single-Page App With GA4

September 11, 2026 · 9 min read

You launch a new site built in React, Next.js or Vue. It is fast, it feels smooth, and a few weeks later someone notices that GA4 reports almost every session as a single page view. Visitors browse five pages, and analytics sees one.

Or the opposite happens: page views double on the first load, and nobody can explain why the numbers jumped after launch.

Both problems come from the same place. Single-page apps change what a "page view" means, and GA4 needs to be told how to recognize one.

Why does GA4 not track page views in a single-page app?

Because in a single-page app, moving between pages does not load a new page. The browser loads the application once, and after that the router swaps content and updates the URL with JavaScript. The GA4 tag runs its page_view on load, and there is no further load for it to react to.

On a traditional site, every click on a link requests a new HTML document. The page reloads, the tags run again, and each page gets its own page_view automatically.

A single-page app works differently. React Router, the Next.js router, Vue Router and similar tools intercept link clicks, render the new view in place, and call the browser's History API to change the address bar. From the visitor's point of view they moved to a new page. From the tag's point of view, nothing happened.

So unless something tells GA4 about each route change, you get one page_view per session, attributed to the landing page, and every page after it disappears from your reports.

How does GA4 enhanced measurement handle route changes?

GA4 enhanced measurement includes a setting under page views called "Page changes based on browser history events". When it is on, which it is by default for page views, GA4 listens for History API changes and sends a new page_view each time the URL changes.

For many single-page apps, this is enough. You will find it in GA4 under Admin, Data streams, your web stream, Enhanced measurement, then the settings for page views.

It has limits worth knowing:

Page titles can lag. If the app updates the document title after changing the URL, the page_view may carry the previous page's title. Your reports then show correct URLs with wrong titles.

URL changes are not always page changes. Filter panels, tabs and search boxes often update query parameters through the History API. Each change can register as a new page_view, inflating views on the pages that use them.

You get no control over timing or data. You cannot add custom parameters, such as a content group, or wait until the new view has finished loading.

It only covers GA4. Other platforms installed through GTM, such as the Meta pixel or the LinkedIn Insight Tag, know nothing about the GA4 setting and still only see the first load.

How do I use the GTM History Change trigger?

Google Tag Manager has a built in History Change trigger that fires whenever the URL changes through pushState, replaceState, popstate or a hash change. Attach your page view tags to it and they fire on each route change as well as on the initial load.

To make it useful, enable the built in history variables: New History Fragment, Old History Fragment, New History State, Old History State and History Source. History Source tells you which kind of change happened, which lets you filter out replaceState calls from filter panels if you only want real navigations.

This approach covers every platform in the container, not only GA4. It shares the timing weakness of enhanced measurement, though. The trigger fires as soon as the URL changes, which may be before the new view has rendered or updated its title.

Should I use a custom dataLayer event for page views?

It is often the most reliable option. The app pushes an event such as virtual_page_view to the dataLayer after each route change, once the new view is ready, and GTM fires your page view tags on that event.

This is a legitimate and common pattern, and it has a clear advantage: one event drives every platform. The GA4 page_view, the Meta pixel PageView, the LinkedIn Insight Tag and any other base pixels can all fire from the same trigger, so they agree with each other about what counts as a page.

The developer pushes the event from the router's navigation hook, usually with the new page path and title included. Because the app knows when rendering has finished, the event can wait until the title and content are correct.

If you use this pattern, turn off "Page changes based on browser history events" in GA4 enhanced measurement. Otherwise both mechanisms send a page_view on each route change.

Why does the first page load send two page_views?

Because two things are sending one. The Google tag sends a page_view automatically when it loads, and the custom event or History Change trigger sends another for the same page. This is the most common failure in single-page app setups.

It usually happens like this. The Google tag is configured normally, so it sends a page_view on initialization. The app also pushes virtual_page_view on first render, because the router treats the initial route as a navigation. GTM fires a GA4 page_view event tag on that. Result: two page_views on every landing page, one on each later page.

There are two clean ways to fix it:

Stop the automatic page_view. Set send_page_view to false in the Google tag's configuration settings, and let the custom event handle every page view, including the first.

Skip the first custom event. Have the app push virtual_page_view only on genuine route changes, not on the initial render, and let the Google tag handle the first load.

Pick one. The first is usually cleaner, because every page view then comes from a single path with the same parameters. Check the Meta and LinkedIn tags as well. If their base code fires on All Pages and also on the custom event, the same doubling happens there.

How do I verify single-page app tracking in GTM preview and DebugView?

Open GTM preview mode, load the site, and click through several pages without reloading. The event timeline on the left should show one navigation event per route change, and each page view tag should fire exactly once per event. Then confirm the result in GA4 DebugView.

In GTM preview, check:

The first load. Exactly one GA4 page_view should fire on the landing page, from whichever source you chose.

Each navigation. One virtual_page_view or History Change event per click, with each page view tag firing once on it.

The values. Click the event and inspect the variables. The page path and title should be the new page, not the previous one.

In GA4 DebugView, you should see a steady sequence of single page_view events, each with the correct page_location and page_title. Two page_views seconds apart for the same URL means duplication. Only one page_view in a session where you visited several pages means route changes are not being captured.

The Network tab gives you a final check. Filter for "collect" and count the requests with en=page_view as you navigate. An audit or scan that clicks through the site can repeat this across many routes.

What does broken single-page app tracking do to reports and ads?

Missing route changes and duplicate page views both distort the data your marketing decisions depend on, in opposite directions.

Missing page views. Every session looks like a one page visit. Landing pages get credit for everything, content that helps visitors convert looks unvisited, and path analysis becomes meaningless. Remarketing audiences built on page visits, such as visitors to pricing pages, stay nearly empty. On Meta and LinkedIn, conversions defined by URL rules, such as reaching a thank you page, never fire if that page is reached through a route change. The platforms then report fewer conversions than happened, and automated bidding learns that campaigns are underperforming when they are not.

Duplicate page views. Views per session inflate, and on sites where the thank you page is also a route, URL based conversions can count twice. Bidding then overvalues whatever traffic produced them.

Either way, spend moves on the basis of numbers that do not describe what visitors actually did.

The plain summary

Single-page apps do not reload, so page views need a deliberate signal. Enhanced measurement history tracking works for simple GA4 setups. A custom dataLayer event is more reliable when you run other ad pixels, as long as you disable the overlapping mechanisms.

Whatever you choose, make sure exactly one page_view fires per page, including the first, and verify it in GTM preview and DebugView before trusting the reports.

FAQ

Does GA4 track page views in a single-page app automatically?

Partly. If enhanced measurement is enabled with "Page changes based on browser history events" turned on, GA4 sends a page_view whenever the URL changes through the History API. That covers many single-page apps, but titles can lag behind URLs, filter changes can register as page views, and other platforms in Google Tag Manager, such as the Meta pixel, will not see the route changes.

How do I track route changes in React or Next.js with Google Tag Manager?

Either use GTM's History Change trigger, which fires when the URL changes through the History API, or have the app push a custom event such as virtual_page_view to the dataLayer after each route change. The custom event is usually more reliable, because the app can wait until the new page has rendered and its title has updated before sending it.

Why is GA4 showing two page views on the first page load?

Usually the Google tag sends its automatic page_view on load, and a custom dataLayer event or History Change trigger sends a second one for the same page. Fix it by setting send_page_view to false in the Google tag and letting the custom event handle every page view, or by having the app push the custom event only on genuine route changes.

Should I turn off enhanced measurement history tracking in GA4?

Turn it off if you send page views through a custom dataLayer event or the GTM History Change trigger. Leaving it on means both mechanisms send a page_view on each route change, doubling your views. If you rely on enhanced measurement alone and have no other page view logic for route changes, leave it on.

How do I test GA4 page views in a single-page app?

Use GTM preview mode and click through several pages without reloading. Confirm one navigation event per click and one page view tag firing per event, with the correct page path and title. Then check GA4 DebugView for a single page_view per page, including the landing page. The Network tab, filtered for "collect", gives you an independent count.

Want help with Google Analytics Audit?

A line-by-line review of your Google Analytics 4 property, events, and conversions, so you know which numbers you can scale on.

More reading

Advanced Tracking

September 17, 2026 · 10 min read

Is Google Tag Manager Slowing Down Your Site?

PageSpeed Insights often blames Google Tag Manager, but the tags inside it are usually the weight. How to read the report, what to fix first, and what server-side tagging really changes.

Read article