Tracking events inside React(-Native) applications using HOCs

Elias el Khaldi
Picnic Engineering
Published in
6 min readMar 16, 2021

--

Tracking user interactions inside of applications and web pages is of great importance for data-driven companies such as Picnic. Applying analytics to this type of event data can help expose interesting statistics and possibly highlight parts of the app that need improvement.

Implementing this can quickly grow to become complex because front-end applications often consist of multiple pages and layers of components. The code is usually separated according to the structure of the application, and to increase overall code quality.
What issues arise when we try to track user interactions? Let’s take button clicks as our example. Adding tracking features throughout the whole app would require the changes to be implemented at each place in the code where a button is created. This would in turn create a large amount of duplicate code, every software engineer’s worst nightmare.

With the use of HOCs (Higher Order Components) in React(-Native) applications, we can help reduce the amount of code needed to track different components. Before we go into this, let’s have a quick recap on Components and HOCs in the React world.

Components

Components in React can be seen as the building blocks for your applications. They let you “split the UI into independent, reusable pieces, and think about each piece in isolation” (source: https://reactjs.org/docs/components-and-props.html). The components can be created using (ES6) classes, but also by using functions. For the functional implementation we take as input some properties and return a UI component, like shown in the example below:

const Cat = (props) => {
return <h1>Hello, I am a {props.color} cat!</h1>;
}

You can then create a Cat component in your code like shown below:

const blackCat = <Cat color=”black” />;
// Output when rendered: “Hello, I am a black cat”

This example creates a simple static component that does not support any user-interaction. This is not always the case of course, many of the components we work with are actually interactive. For example, if we take a look at the Button component we see the following:

<Button onPress={someFunction} title=”Click on me”/>

As you can see, the Button component has different properties, one of them being onPress. The onPress property is simply a function that gets called whenever the button is pressed. In the same way, a Switch component has an onValueChange function, and the list goes on.

HOCs

Now that we have quickly gone through what Components are and how they can be created using functions, let’s have a look at HOCs.

Simply put, an HOC is a function that takes as input a component and returns a component. So instead of creating a function that transforms properties into a (UI) component, you are creating a function that transforms one component into another component, often referred to as wrapping the component. Note that since a component can be defined as a function, an HOC can be defined as a function that takes a function as an input and returns yet another function as an output. As you might have noticed, this is not a feature of React, instead it is a feature that arises from the functional nature of JS. Let’s have a look at how this is implemented.

Let’s have a look at the Cat component we created in the previous example. Because we are superstitious and want to avoid bad luck, we never want to show any black cats. If we find one, we want to change its color to brown. Using an HOC, we can implement this like below:

withoutBadLuck = (WrappedComponent) => {
return (props) => {
if(props.color === ‘black’){
newprops = {… props}
newprops.color = ‘brown’
return <WrappedComponent newprops>
}
return <WrappedComponent props>
}
}

Example of how we can use this:

CatWithoutBadLuck = withoutBadLuck(Cat)cat1 = <CatWithoutBadLuck color=’white’>
// Output when rendered: “Hello, I am a white cat”
cat2 = <CatWithoutBadLuck color=’brown’>
// Output when rendered: “Hello, I am a brown cat”
cat3 = <CatWithoutBadLuck color=’black’>
// Output when rendered: “Hello, I am a brown cat”

We created an HOC called withoutBadLuck that takes as input a component and changes its color to brown whenever it is black. Note that to change the properties we first have to make a new variable called newprops by copying the original props. This is because the props that are received are of type read-only and trying to modify them directly will result in errors.
Furthermore, as we have not strictly defined anywhere that we only accept Cats as input, every component with a “color” property will do.

HOC for tracking

Now that we understand the concepts of Components and HOCs inside React (Native) applications, let’s see how we can use this to track events. It would be nice if we could create an HOC that adds tracking capabilities to every interactive component that is out there.

First of all, let’s define a tracking function.

const trackFunction = (action, name) => {
console.log(
“tracked action “ + action + “ on component “ + name
)
}

In this case, the function takes as input an action-type and a name, both strings. The way you track your events differs per application and in this case, actually, we don’t do anything except log that the function has been called.
A framework that is often used inside Picnic systems is called Snowplow . Snowplow provides tracking functions for multiple platforms, including JS and React, and can be easily integrated with numerous data warehouse platforms. However, in the scope of this article, the method you choose to use for tracking is of no importance.

The next problem we have to tackle is how to add this to a component using HOCs. As we saw in the previous examples, every component can have different property names for their actions. For example, for a Button, the trigger function was assigned to the onPress property, while for the Switch it was the onValueChange property. We thus need a way to make this more generic. This can be done by adding an extra argument to the HOC definition:

const withTracking = (WrappedComponent, triggerFunctionKey, action, idKey) => {

}

The triggerFunctionKey argument tells us which property contains the trigger function. This way we can add our tracking to it whenever it is called. Furthermore, we want the name of the event to be linked to some property as well. As you can see in the code above, we now also have the action and idKey arguments. While action is simply a string which can be set to a value like “BUTTON_CLICK” or “SWITCH”, the idKey actually points to one of the properties of the wrapped component. This way, we are able to derive which component was clicked based on the events. We can then utilize the HOC like shown below:

ButtonWithTracking = withTracking(Button, ‘onPress’, ‘BUTTON_CLICK’, ‘name’)const buttonX = <ButtonWithTracking name=’button1' onPress={someFunction}>// console output when clicked >> “tracked action BUTTON_CLICK on button1”

Alright, let’s finish our implementation of the withTracking HOC so it actually tracks the events when they happen:

const withTracking = (WrappedComponent, triggerFunctionKey, action, idKey) => {
return (props) => {
const newprops = { …props };
newprops[params.triggerFunctionKey] = (…args) => {
const result = props[params.triggerFunctionKey](…args)
trackfunction(action, props[idKey])
return result;
};
return <WrappedComponent {…newprops} />;
};
}

Note that the original function is first called before the other function gets called. This is to make sure that we do not delay the original functionality of the component by calling the tracking function first.

We now have a working HOC that adds tracking to every interactive React(-Native) component you give it. The code is still error-prone and difficult to debug. The next step would be adding typing (Typescript) and error handling to make this more robust, a topic to be covered in a future article.

Tracking at Picnic

At Picnic, we have multiple applications that are implemented using React(-Native). These apps are scattered over different teams, each with their own purpose. As our company grew the need for tracking increased as well. As a result, we now had multiple projects that needed this tracking to be implemented. Subsequently, we had to make the tracking of user interactions as generic as possible, so it could be used across all of these different apps. To do this, we created an HOC that handles all tracking using Snowplow, based on some input arguments not very different from the one’s shown in the example above. Next we released a tracking module on npm that, among other things, included this feature. As a result, each team working on a React(-Native) project can now easily import the tracking functionality as a module and use it in their code. This helped save us development time, but also makes the code a lot cleaner and easier to comprehend. HOCs for the win!

--

--