menu
Trains with lights

Nuxt Enterprise Patterns: The Event Bus

The backbone of most successful websites is a robust marketing strategy. This includes a wide variety of integrations; tag managers, remarketing pixels, analytics providers, session replay services and much more. Many require in-depth enhancements to your website and are often painful to integrate. Luckily they all have one thing in common:

They only want to know every single detail about your users, and then some.

No really, it's crazy; Product views, product list views, how far they scrolled, recommendation clicks, add to cart clicks, subscribe completions, the categories they browsed and MUCH more.

If you try to tackle all of this without a solid plan, then you are creating technical dept as-we-speak.

There is a way to protect your codebase from all of this chaos while distributing data to those who need it. May I introduce: The Event Bus.

How It Works

Instead of having your code talk directly to integrations, they talk to an Event Bus. Integrations listen on the bus for any events they care about. When they find something they like, they send the appropriate data to their destination in whatever format they require.

This is NOT for talking to your own Vue components. This is only for talking to your third party integrations which should be inside Nuxt plugins.

Do You Need An Event Bus?

You'd probably want one if:

  • You plan on implementing Google Enhanced ECommerce
  • You have more than one marketing integration
  • You have an e-commerce website

Standardize Events & Objects

The next step is standardizing your events and objects. Make a list of all events your integrations need and the data each needs. Then, come up with the object structure needed per event that fulfills the needs of all integrations.

If you plan on using Google Enhanced ECommerce, you may be tempted to use that data structure as your base format. That's fine. Just keep in mind it might get weird if you move away from Google for your analytics. Personally, I like using my own format that's not married to any one integration.

Examples
Event: chatClicked
Integrations: Google Analytics, FullStory
Payload: {
  location: HEADER/FOOTER
}

Event: productViewed
Integrations: Google Analytics
Payload: {
  name: 'My Little Pony T-Shirt',
  category: 'Apparel/Men/Adult',
  image_url: 'https://cdn.ponypower.com/abc-pny-xl/main.jpg'
  sku: 'ABC-PNY-XL',
  brand: 'My Little Pony',
  price: '19.95',
  currency: 'usd'

}

Create Your Bus

Enough planning, time to get your hands dirty! We'll start by creating a Nuxt plugin for our event bus.

plugins/bus.client.js
import Vue from 'vue'

export default (ctx, inject) {
  // Yep, that's it.
  inject('bus', new Vue())
}

Update our config file so our bus plugin is always first so all other plugins can use it right away.

nuxt.config.js
extendPlugins (plugins) {
  plugins.unshift('~/plugins/bus.client.js')
  return plugins
}

Use The Bus In Your Components & Integrations

Now for the moment you've been waiting for, let's put this new event bus into action!! Ok, but first..

Note: I HIGHLY recommend moving all of your integrations to plugins using the methods outlined in the post Third Party Code Is Poison. It will keep all of those pesky integrations highly focused and maintainable.

Alright, for real this time, here is an example using our chat event. The header component below emits the event when someone triggers chat.

header.vue
<template>
  <div>
    <button @click="openChat">Chat Now</button>
  </div>
</template>

<script>
export default {
  methods: {
    openChat() {
      // open chat
      this.$chat.open()
      // tell everyone about it
      this.$bus.$emit('chatClicked', { location: 'HEADER' })
    },
  },
}
</script>

Your integrations are listening for this event so they can report it to their overlords.

integrations/ga.client.js
// Your Google Analytics Plugin
export default ({ app }) => {
  // listen for the chatClicked event
  app.$bus.$on('chatClicked', ({ location }) => {
    // give the data to Google
    gtag('event', 'Chat Clicked', {
      'event_category': 'UI Events',
      'event_label': location
    });
  })
}
integrations/fullstory.client.js
// Your Fullstory Plugin
export default ({ app }) => {
  // listen for the chatClicked event
  app.$bus.$on('chatClicked', ({ location }) => {
    // give the data to full story
    window.FS.event('chatClicked', location)
  })
}

Data Construction Warning

It may be tempting to construct your event data in the components that emit them. This is especially true if the component already has access to the data the event needs. PROCEED WITH CAUTION.

product.vue
<script>
export default {
  computed:{
    product(){
      return this.$store.products[id]
    }

  },
  mounted(){
    this.$bus.$emit('productViewed', {
      // This is the path to bad code!!
      name: product.productName,
      category: resolveCategory(product.categoryBreadCrumbs),
      price: centsToDollars(product.price),
      sku: product.sku,
      brand: product.brand,
      currency: 'usd'
      image_url: getMainImage(product.gallery)
    })
  }
}
</script>

Your components should be focused on rending things for the user, not constructing payloads for third party trackers! You want your components to be as simple as possible and only send the bare minimum to the event bus.

Your integrations (plugins) should be constructing the data they need to do their job. This is especially easy if you are already using Vuex.

product.vue
<script>
export default {
  mounted() {
    // Much better
    this.$bus.$emit('productViewed', productId)
  },
}
</script>
integrations/ga.client.js
export default({ app, store }){
  app.$bus.$on('productViewed', emitProductViewed)

  // construct the payload from vuex and send to Google Analytics
  function emitProductViewed(productId){
    const product = store.products[productId]
    const payload = {
      name: product.productName,
      category: resolveCategory(product.categoryBreadCrumbs),
      price: centsToDollars(product.price),
      sku: product.sku,
      brand: product.brand,
      currency: 'usd'
      image_url: getMainImage(product.gallery)
    }

    gtag('event', 'view_item', { "items": [ payload ]});
  }
}

Do multiple integrations need the same chunks of complex data? Just create a helper and use that in each plugin.

Event Bus? More Like The Party Bus!

As you can see the Event Bus is a very powerful tool! By using generic events throughout your app it makes it super easy to tie any integration to an event while keeping everything nice and seperate.

But Wait, Won't $on be killed in Vue 3?!

It's possible $on will be killed in Vue 3 via RFC 0020, I haven't heard anything final. But fear not! You can replace it in just a few lines of code using Mitt.

plugins/bus.client.js
import mitt from 'mitt'
const emitter = mitt()

export default (ctx, inject) => {
  inject('bus', {
    $on: emitter.on,
    $emit: emitter.emit
  })
}

Oh, There Is One Last Thing

With great power comes great...overuse of movie phrases.. You may be thinking to yourself, "hmm, couldn't I just use an Event Bus across my entire application for internal communication across all of my components instead of using this.$emit?"

Why yes, yes you could.

Would that be horrible? Yes, yes it would.

Resist the urge to use the Event Bus for internal communication, that's an anti-pattern. Only use it to transport event information to your integrations. Your events shouldn't be "controlling" anything in your app. You should be able to remove all of your $bus.$emits and $bus.$ons and have your application still function properly.