DEV Community

Bryan Lee
Bryan Lee

Posted on

Vue Renderless Component Fun: Google Analytics Event Tracker

Let's say you needed to track how many times a specific button in your Vue app is clicked on using Google Analytics, how would you do it? The first thing that comes to mind might be with a click handler.

<button @click="onClick">Important Button</button>
methods: {
  onClick(e) {
    ga('send', 'event', 'Video', 'play')
    // Other things the button does when clicked
  }
}

This is a perfectly fine approach, but as you start tracking more button and link clicks, this can get cumbersome. With renderless components in Vue, we can abstract the event tracking into its own easy-to-reuse component.

What is a Renderless Component

A normal Vue component has a HTML template, Javascript and CSS. A renderless component does not have its own markup. It usually has a render function that renders a scoped slot. Data and function from the renderless component gets passed to the child component through the scoped slot. This simple concept essentially lets you to abstract out complex behaviors that can be reused with different components.

To illustrate this better, we will build a renderless component that you can wrap around buttons, links and other elements to track clicks in Google Analytics. Let's start off by scaffolding a renderless component we shall call Track.

// Renderless component
// Track.js
export default {
  render() {
    return $this.$slots.default;
  }
}
// App.vue
<template>
  <div>
    <track>
      <button @click="onClick">Important Button</button>
    </track>
  </div>
</template>

<script>
import Track from 'track';

export default {
  components: { Track },
  methods: {
    onClick(e) {
      // the button's own click handler function
    }
  }
}
</script>

We import our new Track component into our App component, and wrap it around the <button> element which we intend to track. Track here is a very basic renderless component. All it does is render a default slot, in this case, rendering the <button> which we've wrapped around with. If you refresh your page now, you would not notice any difference. The Track component is transparently rendering the child button, it has no markup of its own.

Listening to Clicks

Now, we want to start making our Track component useful. We want it to listen for clicks on its child element and then send call the Google Analytics API when there is one.

// Track.js
export default {
  render() {
    return $this.$slots.default;
  },
+ mounted() {
+   this.$slots.default[0].elm.addEventListener('click', this.trackClick, true);
+ },
+  methods: {
+    trackClick() {
+      ga('send', 'event', 'Video', 'play');
+    }
+  }
}

Let's go through what we just did. We are declaring in our component's mounted lifecycle hook after it is rendered that we are adding an event listener. this.$slots.default means that we are accessing the component's default slot. We are looking for only the first element, and we add an event listener for click events. If there is a click, we run the trackClick function.

The last argument in addEventListener is true says we want to use event capturing instead of the default event bubbling. Event capture means that events are dispatched top down the DOM tree to our listener instead of the default bubbling up the DOM tree. In actual effect, this lets us also listen to clicks even if there is a preventDefault declared.

Making it More Reusable

There is one slight issue left. What if we want to track another link in another page? Looking at our code again, all clicks will be calling this ga('send', 'event', 'Video', 'play'). Let's make it so that we can customise the different variables we want to send over to Google Analytics (GA) using props. We shall also follow the standard fields set by the GA API.

// Track.js
export default {
  render() {
    return $this.$slots.default;
  },
+ props: [
+   eventCategory,
+   eventAction,
+   eventLabel,
+   eventValue,
+ ],
  mounted() {
    this.$slots.default[0].elm.addEventListener('click', this.trackClick, true);
  },
  methods: {
    trackClick() {
-     ga('send', 'event', 'Video', 'play');
+     ga('send', 'event', eventCategory, eventAction, eventLabel, eventValue);
    }
  }
}

Now, we can use it in different places with the relevant event fields:

<Track eventCategory="Checkout" eventAction="Button Click" eventLabel="Sidebar" eventValue="$30">
  <button>Some button</button>
</Track>

Happy Tracking!

Our Track component is now ready to use in other parts of the code. One advantage of abstracting out external APIs you gain is that code becomes more maintainable now. Google made an update to the GA API that requires an update? Instead of updating the 30 different places where the API call is used, you just update it in Track.

Another huge plus, your code base is more inclusive. Previously, a new developer that joins your team will also need some familiarity with the GA API on top of their Vue skills. Abstracting this into an intuitive concept that most Vue developers are already familiar with equals to more productivity from day one.

This post first appeared on Bryan Lee.

Top comments (1)

Collapse
 
thomasfindlay profile image
Thomas Findlay

You should also add removal of the listener as otherwise this code will introduce memory leak.

const el = yourlistener
this.$on('hook:beforeDestroy', () => removeYourListener)