Using JS Intersection Observer API to track user footprints

Harshit Thukral
5 min readMay 30, 2021

Understanding user behavior for your web app to find out where the disconnect is and which of your features is giving a hard time to your users is no more a secondary thing. If you try googling for some good ready to integrate solutions to track users, you will find some big and established players like Google Analytics and Mixpanel who also serve you with exceptional metrics and dashboards based on the data you publish to them. Now, your respective teams can analyze this data and zoom into the actual pain points and gaps.

But what if you had a use-case like we did, where a user had to pay for each visit depending on the time they spent and the features they strolled over during their time on the platform. The question that comes out, is this data first of all exposed and secondly reliable enough to cut someone a ticket? The answer was NO! All because integrating most of these libraries effectively requires a lot of SDK calls to be integrated across your whole app like landmines. So without boring you any further with the back story of why let’s jump to..

What

After weighing the effectiveness and integration efforts we decided to rely on the browser’s IntersectionObserver API to rescue us. It lets you observe elements in your DOM tree and dispatches an event when one of those elements enters or exits the viewport. Let’s put some code where my mouth is.

Tracking Service

First, we needed a service that can work as a singleton to observe and track different components in your viewport and also independently integrate with the backend service.

So, what’s happening here is, we created function createObserver an abstraction that exposes two fundamental methods:

observe: this will help us register our components/nodes to the observer. So that it can start tracking and notify us once the state of the element changes.
unobserve: Just opposite to the observe method. its job is to de-register the element from the observer and stop if there's any already running timer.

Now, these two simple methods work for most of the cases, but there’s one particular case, when the whole app un-mounts and we still had few running timers. In that case, we need to maintain an in-memory map of all elements being tracked and expose another method unobserveAll that would just unobserve all records before going down.

With the new code additions, we now have a map called observingTargets that holds all the elements under observation and their current state. When any of those elements change state, for each of them, we update the record, and a boolean isIntersecting property telling the current state. The only thing remaining now is to hit the backend service API to start/stop the timer. Let's add that as well and then we can rub our hands and integrate it with our react components.

React HOC

On the UI component side of things, one has to handle three things:

  • Register itself to the observer service using observe and tell it to keep an eye on the component's intersection with the viewport.
  • Use unobserve function to de-register itself before un-mounting
  • Call unobserveAll function that will stop all the running timers once a user decides to leave your app.

The third one can be handled using the window’s beforeunload event, which is called right before the tab unloads. So, for our React components, we'll be focussing on the first two.

HOC stands for Higher-Order Component. It’s not something specific to React and lets you extend your components compositionally. As per official React documentation:

Concretely, a higher-order component is a function that takes a component and returns a new component.

So let’s implement it:

What we implemented above is a function that returns our custom component, which renders the very same component in the render method that needs to be tracked and was passed to it as the first param. Additionally, it takes care of both registering(observe) and unregistering(unobserve) the actual DOM node using component lifecycle hooks.

PS: This can also re-written using a lot of React Hooks shorthands, you can try, but I find it easier to convey the message with the legacy API.

Now let’s see how it can be used with our components:

That’s it. Now, all we need to track our components is to wrap them with the TrackedEL HOC that will take care of all observing and un-observing logic using the functions exposed by the timer service created above.

So, now at the end of it, we have a well-crafted, easy to integrate, and extensible way to track our components and in-premise user data that can be relied upon as well as easily reconciled.

You can find the whole working code in this sandbox. Suggestions and corrections would be really appreciated. Here’s the link if you are facing trouble copying 😉.

Keep Tracking.

--

--