React SPA Analytics: Tracking Route Changes the Right Way
Single-page apps break naive pageview tracking. How modern trackers detect client-side navigation in React, and which metrics matter for SPA performance.
Install a classic analytics snippet on a React single-page app and you get a flattering, useless dataset: one pageview per visit, a bounce rate near 100%, and session durations of zero — because the browser never reloads, and the tracker never notices the user navigating. Half the 'our analytics seems broken' emails we receive reduce to this. Here is how SPA tracking actually works, and what to measure once it does.
Why classic trackers go blind in SPAs
Traditional analytics fires on the page load event. In a React app with client-side routing (React Router, TanStack Router, Next.js Link navigation), that event happens exactly once. Every subsequent 'page' is a DOM mutation plus a history.pushState() call — invisible to a tracker that only listens for loads.
The fix: History API instrumentation
A modern tracker patches pushState and replaceState and listens for popstate:
// Simplified — what trackers like Clycyo's do internally
const original = history.pushState;
history.pushState = function (...args) {
original.apply(this, args);
recordPageview(location.pathname); // new virtual pageview
};
window.addEventListener('popstate', () =>
recordPageview(location.pathname));Clycyo's tracker ships this built in, so installation in a React app is the same single script tag as anywhere else — no router integration, no useEffect hooks per page, no manual pageview calls to forget. (Framework-specific notes live in the Next.js guide and the quickstart.)
SPA performance is a different animal — measure it differently
In a multi-page site, 'page speed' is load time. In a SPA, only the first hit has a load time; every navigation after that is a render. Useful SPA metrics therefore split in two:
- Initial load: navigationStart → loadEventEnd, dominated by your bundle size. This is where the 100 KB analytics suite hurts twice — it slows the very metric it is supposed to measure.
- Route transitions: frame time around the navigation — how long the main thread stalls while React unmounts, fetches, and re-renders. Clycyo records this per route change, so a janky /dashboard transition shows up next to the pageview it delayed.
- INP: of the Web Vitals, Interaction to Next Paint is the SPA truth-teller, because SPAs concentrate their slowness in interactions rather than loads.
Three SPA-specific gotchas
- Redirect chains create phantom pageviews. Auth flows that bounce through /callback → /loading → /app in 200 ms inflate counts. Either let them (they are real navigations) or filter the transient routes in reporting — just be consistent.
- Query-param navigation may not be a pageview. If /search?q=a → /search?q=b should count as one page, check how your tracker treats query strings.
- Error boundaries hide crashes from your error tracking. A crashed component that shows a fallback UI is invisible in pageview data. A tracker that captures JavaScript errors on the same visitor record (as Clycyo does) shows you the error and the rage-refresh that followed it, in sequence.
The payoff: journeys that make sense
Once route changes are real pageviews, the per-visitor timeline becomes legible: landed on /pricing from the newsletter, viewed /docs, hit a TypeError on /signup (eight seconds before abandoning), came back the next day, converted. That story — behavior, performance, and errors interleaved — is precisely the data a product engineer needs, and it is the core of what Clycyo puts on one screen.