Single-page and Multi-page Applications: What's the difference?
In virtually every website that is not an SPA, loading a new page requires completing a full request-response cycle between the client and the server. Whenever the user clicks a link, the browser sends an HTTP GET request to the server for the content the user has requested. In an SPA, however, the browser loads all of the resources it needs to deliver the full experience of the site on the first-page load.
After that, when the user navigates to a different page or requests resources that are not yet in the UI, SPAs leverage HTML5 APIs, AJAX, and the capabilities of modern browsers to only serve the new resources that aren't yet on the page while the rest of the content stays the same. This is especially useful in interfaces that repeatedly display new content as a user scrolls down a feed. Examples of such use cases in our day-to-day experience of the web are plentiful––think scrolling through new Tweets, looking through product results on Amazon, or checking your email.
This method of dynamically presenting new content hands developers some challenges when it comes to collecting robust analytics. Traditionally, websites send user events to analytics tools via<script> or <link> tags that fire when the page loads, but this is not an option in an SPA if you want to track each occurrence of a user landing on a new page.
This is especially an issue with page view tracking, which is one of the most important data points to capture when building a picture of how your customers interact with your site.
Tracking Page Views in a React App
Since React is an extremely popular (if not the most popular) framework choice for building SPAs, the rest of this post will dive into a strategy for tracking page view analytics in a React app that is simple, maintainable, and scalable.
We’ll use this same sample application––the website of a simple furniture store that includes a homepage and four landing pages for tables, desks, chairs and lamps. Importantly, the application also uses React Router to handle page navigation.
While the method for collecting navigation events we will explore can be used to send data to any third-party tool, in this example app, we will forward these customer events to mParticle––a Customer Data Platform that allows you to collect customer data once through secure APIs and SDKs and connect it to all of the tools in your stack.
Here is what our furniture store's UI looks like:
As we can see, our navigation events are being tracked and sent to the mParticle dashboard. Here's how we do that:
As we know, our React application is serving each page dynamically, so we cannot use page loads to trigger event tracking APIs. What we can do, however, is call the appropriate methods during the mount phase of the component lifecycle.
Since we are using functional rather than class components in our application, we will pass our page tracking method as a callback function to the Effect Hook with useEffect(). Here’s what the component that renders our chairs page looks like with the addition of calling the logPageView method on mParticle’s web SDK:
function Chairs() {
useEffect(()=> {
window.mParticle.logPageView(
"Chiars Page",
{page: window.location.toString()}
);
}, [])
return (
<div>
<h1 className="main-heading">Chairs</h1>
<div className="product-image-container">
{Object.keys(products.chairs).map(chair => {
return (
<Product
backgroundImage={products.chairs[chair].image}
productCategory="chairs"
product={products.chairs[chair]}
/>
);
})}
</div>
</div>
);
}
export default Chairs;
Notice that the mParticle logPageView method uses the location property on the window object to dynamically access the page URL via the browser. This is a good strategy to employ regardless of the location to which you are sending the data, as it helps make the API call more reusable and dynamic.
Making it Scalable
This accomplishes what we want for the chairs page, and since we only have three more pages to track in our app at the moment, it wouldn’t be hard to replicate this code in each additional component. Though what happens when our furniture store grows, and our site includes dozens, if not hundreds more pages? It would not be optimal to require every developer on our team to remember to add this useEffect hook to each new page. Ideally, we would be able to implement the page view tracking once in our app, and not have to worry about it as we build out additional page components.
Luckily, by using React Router combined with the useHistory hook, we can do just this:
function Routes(props) {
const history = useHistory();
useEffect(() => {
trackPageView();
history.listen(trackPageView); // Track all subsequent pageviews
}, [history]);
function trackPageView() {
window.mParticle.logPageView(`${window.location.pathname}`, {
page: window.location.toString(),
});
}
return (
<Switch>
<Route exact path="/" component={Home} />
<Route exact path="/tables" component={Tables} />
<Route exact path="/desks" component={Desks} />
<Route exact path="/chairs" component={Chairs} />
<Route exact path="/lamps" component={Lamps} />
</Switch>
);
}
export default Routes;
This is our Routes component, where we are mapping a specific path to each of the components that render our pages. At the top of this function, we create an instance of the “history object,” which is a dependency of React Router that provides a useful way to manage session history in a React application. Next, we move the API call we want to make on each page view into its own trackPageView helper function where we use window.location to dynamically pass the URL and page into the payload sent to mParticle. Finally, we add a call to useEffect, where we:
Call trackPageView() to run the first time the component renders.Pass trackPageViews into the listen method on the history object to make sure every page view is tracked.Call the trackPageViews function inside of useEffect, and pass the history object to its dependency array specifying that useEffect should be called every time history is updated.
Now we are able to track page views across our application with a simple addition to our Routes component. We’re sticking to the principles of “DRY” programming by implementing functionality once and using it repeatedly, and we got around the challenge of tracking page views in an application that dynamically renders content in the browser.
Finally, a quick side note––useEffect() suffices for the purposes of capturing a page view event, since like its class component counterpart componentDidMount(), useEffect runs after the component mounts. However, it is important to understand that useEffect() is fired after the DOM elements have actually been rendered on the screen, whereas componentDidMount() runs before this occurs. This means that if you need to synchronously extract some data from the DOM, set state with this data, then build a new UI based on the state update, you will actually encounter a page flicker if you use useEffect() to trigger this sequence. In these instances, the useLayoutEffect() method will much more closely resemble the rendering behavior of componentDidMount().
Feel free to take a closer look at this sample application, and experiment with tracking more data and events in a React application. Happy coding!
Previously published at https://www.mparticle.com/blog/react-app-navigation-tracking