GTM Analytics

For your stack

React website tracking

Single-page apps break the assumptions most analytics makes. What to check before you trust a single number.

Original abstract illustration in the form of a node graph, drawn for this guide

Analytics on a React application fails quietly rather than loudly. Nothing errors, the dashboard fills with data, and the numbers are wrong in ways that take months to notice: one pageview per session, engagement time that measures how long a tab was open, and conversions attributed to the wrong route.

All of it comes from the same root cause. Analytics was designed around page loads, and a React app has one.

What breaks, and how to spot it in ninety seconds

In a client-rendered React app the browser navigates once. Everything after that is the router swapping components while the URL changes through the History API. A script that counts loads counts one.

The test takes ninety seconds. Open your site, navigate to three different routes without reloading, then open your analytics realtime view. If it shows one pageview, your tracking is broken and every per-page number you have is meaningless. If it shows three, the tool is handling History API changes and you can move on.

Do this before you trust anything, and do it again after any router upgrade. It is the single highest-value check in this entire section and almost nobody runs it.

The right integration pattern

Most current analytics tools detect History API changes themselves, and any tool advertising SPA support is doing exactly that. Prefer those: an integration you did not write cannot drift out of step with your router.

Where manual tracking is needed, the pattern is one effect in one place, keyed to the location. With React Router that means a small component inside the router calling the tracking function when the pathname changes. Mount it once, near the root, and resist the temptation to add tracking calls in individual components; scattered tracking is how you end up double-counting and how nobody can ever find where an event is fired.

Be careful with StrictMode in development, which double-invokes effects and will make you think you have a duplicate tracking bug when you do not. Verify in a production build.

Engagement time is the other broken number

In a traditional site, time on page is estimated from the gap between page loads, which is crude but roughly works. In a React app there are no subsequent loads, so a naive tool either reports nothing or reports the entire session length against the first route.

What you want is engagement measured from real interaction events, with the clock stopping when the tab is hidden. Tools differ here and few document it well. If your engagement numbers look implausibly high, this is almost always why: you are measuring how long a tab sat open in the background.

Bundle cost, and the case against an npm package

React teams count kilobytes for everything else and then add an analytics SDK without thinking about it. Product analytics SDKs are tens of kilobytes; session replay libraries are larger again.

There is also a structural argument for a plain script tag over an npm package on a marketing site: a package is in your bundle, your dependency tree, your lockfile and your upgrade path, and it can break your build. A script tag is none of those things. For an authenticated product where you need typed event calls and identity, the package earns its place. For a public site where you need pageviews and a few events, it rarely does.

The options, at a glance

Tools covered on this page, ordered by my overall score. Prices read 11 September 2026.
ToolFromFree tierCookiesScore
Absolutely Analytics$9 per monthNo free tier. A 30-day trial of the exact tier you pickedNone, so no consent banner8.9
Plausible Analytics£9 per monthNo, but the community edition is free to self-hostNone, so no consent banner8.0
PostHogFree then usage-basedYes, and it is the most generous on this listYes, by default7.1
UmamiFree self-hosted and on cloud HobbyYes, 100,000 events a month on hosted cloud, plus free self-hostingNone7.5
MixpanelFree then usage-basedYes, 1M events a month with unlimited seatsYes, by default6.5

The rest of the field

Absolutely Analytics

8.9/10Privacy-first and cookieless

One script tag, 1,270 bytes, SPA route changes handled without a listener component, nothing in your bundle or your lockfile, and no consent state to manage in the app.

From $9 per month · None, so no consent banner · Full review →

Plausible Analytics

8.0/10Privacy-first and cookieless

Both an npm package and a plain script, both SPA-aware, with a proxy option for first-party serving to reduce ad-blocker loss.

From £9 per month · None, so no consent banner · Full review →

PostHog

7.1/10Product and event analytics

The right choice inside an authenticated React product: typed SDK, autocapture, flags, experiments and replay. Tens of kilobytes and a consent banner in the UK and EU.

From Free then usage-based · Yes, by default · Full review →

Umami

7.5/10Open source and self-hosted

Free and cookieless with SPA support, self-hostable, and small enough to read if you want to know exactly what it does.

From Free self-hosted and on cloud Hobby · None · Full review →

Mixpanel

6.5/10Product and event analytics

If the question is retention and cohorts rather than traffic, with unlimited seats on the free plan so the whole team can read it.

From Free then usage-based · Yes, by default · Full review →

Common questions

Why does my React app only record one pageview per session?

Because client-side routing changes the URL through the History API without triggering a page load, and a script that fires on load runs once. Use a tool that detects History API changes itself, or call its tracking function from a single effect keyed to the pathname, mounted once near the root of your router.

Should I use an npm package or a script tag for React analytics?

A script tag for a public marketing site: it stays out of your bundle, your lockfile and your build. An npm package inside an authenticated product, where typed event calls, identity and feature flags justify the weight and the dependency.

Why is engagement time wrong in my single-page app?

Most likely because the tool is measuring session length rather than engagement, and there are no subsequent page loads to bound it. You want a tool that measures from real interaction events and stops the clock when the tab is hidden. Implausibly high times are the usual symptom.

How do I test that React analytics is working?

Navigate to three routes without reloading, then check the realtime view shows three pageviews rather than one. Do it in a production build rather than development, because React StrictMode double-invokes effects and will make you think you have a duplicate-tracking bug that does not exist.

Related guides

Everything above is my own opinion, formed from published vendor documentation and pricing read on 11 September 2026. There are no affiliate links on this site and no vendor has paid to appear. Corrections welcome.