Tracking pageviews in single page apps (SPA)
Oct 17, 2022
Estimated reading time: 12 minutes ☕☕
A single page application (or SPA) is an app that lives on a single page. Users can navigate around the app and change the content without having to load new pages. Single page apps rely heavily on caching and components like loading screens compared to other types of apps like progressive web apps.
Single page apps come with some unique challenges. A big one is that since pages don’t need to be reloaded to change, pageviews are often not captured properly. Developers of single page apps need to set up custom events to trigger pageviews at the right time.
In this tutorial, we’ll be creating a single page app, setting up PostHog, then providing multiple ways to capture pageviews. This includes triggering events on page renders, using the router, and watching for component visibility on scroll.
Note: although we’ll be using React for this tutorial, it is relevant to other web SPA frameworks (like Vue, Svelte, or Meteor), mobile apps (Android, iOS), and apps with non-standard navigation. You can find our full list of client libraries here.
Setting up a single page app in React
We’ll start by setting up a basic React app with a router and a couple of pages. To start, go to the command line (or terminal), create a folder for our project, then create the react app (named spa_client), and start the app.
mkdir spa_pageview_appcd spa_pageview_appnpx create-react-app spa_clientcd spa_clientnpm start
If successful, going to localhost:3000 should show you the React logo.
Next, we’ll install the packages we need for the router and PostHog. Back in your terminal install posthog-js and react-router-dom
npm install --save posthog-jsnpm install --save react-router-dom
Once we’ve done this, we can start to work on the content of the application, and routing between each of the pages.
Setting up the router and pages
First, in src/index.js we’ll import BrowerRouter and then wrap our app in it. The BrowserRouter will turn our component into a single page app so we can add multiple pages. Your index.js file should look like this (we’ve removed some unnecessary code like css and webVitals for now):
// src/index.jsimport React from 'react';import ReactDOM from 'react-dom/client';import App from './App';import { BrowserRouter } from 'react-router-dom'; // newconst root = ReactDOM.createRoot(document.getElementById('root'));root.render(<React.StrictMode><BrowserRouter> {/* new */}<App /></BrowserRouter></React.StrictMode>);
Next, we’ll create a couple of basic pages as functions. Create Home.js, About.js, and Benefits.js all with a basic format like this (replace Home with the other names):
// src/Home.jsexport function Home() {return (<><h1>Home</h1><p>Welcome to the Home page</p></>);}
Once you’ve done this, go to App.js and get rid of the boilerplate. We are going to add our routes and navigation to each of the routes here. You’ll need to import each of the pages we’ve created as well as Routes, Route, and Link from react-router-dom. We’ll add each of the pages as a route and a link in our nav. Once you’ve done all of this, your App.js file should look like this:
// src/App.jsimport { Route, Routes, Link } from 'react-router-dom';import { Home } from './Home';import { About } from './About';import { Benefits } from './Benefits'function App() {return (<><nav><ul><li><Link to="/">Home</Link></li><li><Link to="/about">About</Link></li><li><Link to="/benefits">Benefits</Link></li></ul></nav><Routes><Route path="/" element={<Home />} /><Route path="/about" element={<About />} /><Route path="/benefits" element={<Benefits />} /></Routes></>);}export default App;
Once done, checking our local site again should give us a single page app with a few pages. Although basic, it’ll help us illustrate what you need to do to set up tracking and pageview events correctly.
Setting up PostHog
Our last step before setting up correct pageview tracking in this SPA is setting up PostHog. To do this, go back to index.js, import PostHog (we installed it earlier), and initialize it using your project key and host.
// index.jsimport React from 'react';import ReactDOM from 'react-dom/client';import App from './App';import { BrowserRouter } from 'react-router-dom';import posthog from 'posthog-js'; // newposthog.init( // new'<ph_project_api_key>', { api_host: '<ph_instance_address>' })const root = ReactDOM.createRoot(document.getElementById('root'));root.render(<React.StrictMode><BrowserRouter><App /></BrowserRouter></React.StrictMode>);
Once we’ve initialized PostHog, autocaptured events should start flowing into our instance. We’ll also see that we have some pageview events, but clicking our nav links doesn’t trigger pageview events.
![]()
Because this is a single page app, navigation does not trigger new pageview events. You see we clicked the button to go to the Benefits and About pages, but didn’t get pageview events for either of them. We will have to set up custom events to trigger them. In the next step, we’ll go over some ways to do this.
Capturing pageviews
Although autocapture does a lot we’ll have to write more code to capture pageviews in our single page app. We’ll go over a few ways of triggering the pageview events: the router, page render, and on visibility change.
Method 1: router
The first method is using the router. The router allows us to add functions that run every time the page changes. With react-router-dom, we can use useLocation for this. We’ll add a location variable we get from the router, and run a useEffect to trigger a pageview every time it changes.
This is what it looks like in App.js
// src/App.jsimport { Route, Routes, Link, useLocation } from 'react-router-dom'; // newimport { Home } from './Home';import { About } from './About';import { Benefits } from './Benefits'import * as React from 'react'; // newimport posthog from 'posthog-js'; // newfunction App() {let location = useLocation(); // newReact.useEffect(() => { // newposthog.capture('$pageview')}, [location]);return (<><nav><ul><li><Link to="/">Home</Link></li><li><Link to="/about">About</Link></li><li><Link to="/benefits">Benefits</Link></li></ul></nav><Routes><Route path="/" element={<Home />} /><Route path="/about" element={<About />} /><Route path="/benefits" element={<Benefits />} /></Routes></>);}export default App;
Now when we move between pages, we’ll trigger pageviews on each.
![]()
Note: other frameworks or languages have ways to “listen” for the changes in the router that we use to trigger a pageview event. For example, in Vue, you can set up a
watcherand in Svelte, you can use thenavigatingstore.
Method 2: page render
A more manual way to trigger pageview events is by setting them up to trigger every time a page is rendered. You may want to do this if you have a smaller number of pages and only want some of them to trigger pageview events.
To set this up, add a useEffect hook to the page we want to capture. It should look like this:
// src/Benefits.jsimport posthog from 'posthog-js'; // newimport React from "react"; // newexport function Benefits() {React.useEffect(() => {posthog.capture('$pageview') // new}, [])return (<><h1>Benefits</h1><p>Welcome to the Benefits page</p></>);}
Once this is done on each of the pages you want to trigger events, rendering that page will create a pageview event.
Method 3: visibility
Many single page apps navigate through scrolling. As users scroll through the app new content, sections, and components are shown to them. This isn’t automatically captured with PostHog, but we can set up a way to capture it. We’ll do this by checking if the component is visible and triggering a pageview event if so.
There are a few ways to trigger pageview events based on visibility, but we are just going to pick a simple one for this tutorial. We could use IntersectionObserver or set up JQuery to track when elements are in the viewport, but we will just install a react-visibility-sensor component. To start, we’ll install the module.
npm install react-visibility-sensor
Next, we’ll modify our App.js page a bit, so it is one long, scrollable page. We’ll add some tall divs to split up the page.
// src/App.jsimport { Home } from './Home';import { About } from './About';import { Benefits } from './Benefits'function App() {return (<><Home /><div style={{height: '50rem'}} /><About /><div style={{height: '50rem'}} /><Benefits /></>);}export default App;
Finally, we’ll wrap each of the components in a VisibilitySensor and have the change trigger a pageview event.
Note: you can trigger whatever event you want, such as
screenview, if you didn’t want to combine it with the autocaptured pageview events.
// src/App.jsimport { Home } from './Home';import { About } from './About';import { Benefits } from './Benefits'import VisibilitySensor from 'react-visibility-sensor'; // newimport posthog from 'posthog-js';function App() {function onChange (isVisible) { // newif (isVisible) posthog.capture('$pageview');}return (<><VisibilitySensor onChange={onChange}><Home /></VisibilitySensor><div style={{height: '50rem'}} /><VisibilitySensor onChange={onChange}><About /></VisibilitySensor><div style={{height: '50rem'}} /><VisibilitySensor onChange={onChange}><Benefits /></VisibilitySensor></>);}export default App;
Scrolling the app will now trigger a pageview every time one of the components is visible.
Next steps
Now that we are capturing pageviews, we can figure out how users move around our app. For example, we could analyze and optimize our conversion funnel. See a tutorial on how here.
We can also track more events than just pageviews. The event tracking guide will provide you with all the details on how.
For more advanced PostHog users, you can use all of your new pageview data to help you build an AARRR dashboard.

