Global Events in Vue 3

16 May, 2022 6 minute read

You're probably already familiar with Emitting and Listening to events in Vue 3, but you know that these are limited to the neared parent/child. You have to listen for events on the parent, and emit them from the children components. While this limitation has its reasons and use-cases, it can lead to unnecessary code and complexity once your app gets bigger.

Sometimes you just need to get a simple message to another component and you don't know where it'll be in the app, nor do you care, really. Adding Vuex or Pinia can also seem like an overkill just to send a simple message to another component.

Enter Global Events.

Keep in mind, though, that Vue team discourages the use of Global Events because of potential maintenance headaches in the future. Only use this as a short-term solution, and if you understand the potential consequences.

It's really just a Pub/Sub pattern

Events and Listeners in Vue 3 and other frameworks follow the Pub/Sub software pattern, where an object can subscribe (Sub) for updates from another object, which can later publish updates (Pub) to its subscribers.

That is the base principle, but the actual implementations in Vue, Laravel and other frameworks are often more complex, providing improved developer experience, debugging, or other integrations.

Now let's see how you can implement the simplest form of Pub/Sub in your project.

Custom EventBus class

The quickest and purest way we can get started with global events in Vue 3 is to create our own EvenBus class.

Vue 3 does not support Global Events by default, and going outside the realm of official support means you might lose on some benefits, such as seeing those events in DebugTools.

If that is a sacrifice you're willing to make for the same of easier implementation, then here it is.

Create an EventBus.js file with these contents (TypeScrtipt example below):

class EventBus {
  constructor() {
    this.events = {};
  }

  on(eventName, fn) {
    this.events[eventName] = this.events[eventName] || [];
    this.events[eventName].push(fn);
  }

  off(eventName, fn) {
    if (this.events[eventName]) {
      for (let i = 0; i < this.events[eventName].length; i++) {
        if (this.events[eventName][i] === fn) {
          this.events[eventName].splice(i, 1);
          break;
        }
      }
    }
  }

  emit(eventName: string, data) {
    if (this.events[eventName]) {
      this.events[eventName].forEach(function (fn) {
        fn(data);
      })
    }
  }
}

export default new EventBus();

EventBus.ts file contents (if you're using TypeScript):

class EventBus {
  events: {[key: string]: any};

  constructor() {
    this.events = {};
  }

  on(eventName: string, fn?: any) {
    this.events[eventName] = this.events[eventName] || [];
    this.events[eventName].push(fn);
  }

  off(eventName: string, fn?: any) {
    if (this.events[eventName]) {
      for (let i = 0; i < this.events[eventName].length; i++) {
        if (this.events[eventName][i] === fn) {
          this.events[eventName].splice(i, 1);
          break;
        }
      }
    }
  }

  emit(eventName: string, data?: any) {
    if (this.events[eventName]) {
      this.events[eventName].forEach(function (fn: any) {
        fn(data);
      })
    }
  }
}

export default new EventBus();

As you can see from the EventBus.js code, we are exporting an instance of the EventBus class, which means that every Vue component or other JavaScript file that imports EventBus will share the same object instance. This is what we call a singleton.

Using EventBus

Once you have created the file with the contents above, all you have to do is import it and use it!

For example, you can emit new events like so:

import EventBus from './EventBus'

// ...

EventBus.emit('titleUpdated', {
  newTitle: 'New blog post title'
})

// or, without any options
EventBus.emit('exampleEventName')

And to listen to an event, just call on:

import EventBus from './EventBus'

// ...

EventBus.on('titleUpdated', (event) => {
  this.newTitle = event.newTitle
})

// or, with object destructuring
EventBus.on('titleUpdated', ({ newTitle }) => {
  this.newTitle = newTitle
})

A single 30-line file and we can already send global events between our components. Nifty!

Avoiding imports

You can also make the global event bus available automatically on every Vue 3 component. You just need to add it to the globalProperties option when setting up Vue:

import { createApp } from 'vue'
import EventBus from './EventBus.js'
import App from './App.vue'

const app = createApp(App)

// the 'event_bus' property will be available on every component
app.config.globalProperties.event_bus = EventBus

app.mount('#app')

Once set up, you can access it later like so:

this.event_bus.emit('exampleEventName')

If for some reason you're not comfortable adding the EventBus file to your codebase, or would like a more battle-tested solution, you can always go for an NPM package.

The 'mitt' package

If you like installing packages, this will be an easy win for you.

Learn more about mitt here - https://github.com/developit/mitt

Setting up mitt for your project is really easy:

import { createApp } from 'vue'
import mitt from 'mitt'
import App from './App.vue'


const emitter = mitt()
const app = createApp(App)

app.config.globalProperties.event_bus = emitter
app.mount('#app')

If you're using Intertia.js

Setting it up for Intertia.js can also be done easily:

import { createApp, h } from 'vue'
import { createInertiaApp } from '@inertiajs/inertia-vue3'
import mitt from 'mitt'

createInertiaApp({
  resolve: name => require(`./Pages/${name}`),
  setup({ el, App, props, plugin }) {
    const appInstance = createApp({ render: () => h(App, props) }).use(plugin)

    // Here we are creating a new instance of Mitt...
    const emitter = mitt()
    // ... and assigning it as a global Vue property
    appInstance.config.globalProperties.event_bus = emitter

    appInstance.mount(el)
  },
})

Using mitt

Once you have set up mitt, you can emit events like so:

this.event_bus.emit('titleUpdated', {
  newTitle: 'The new blog post title'
})

// or, without any event data
this.event_bus.emit('exampleEventName')

You can listen to events like so:

this.event_bus.on('titleUpdated', (eventData) => {
  this.newTitle = eventData.newTitle
})

// with object destructuring
this.event_bus.on('titleUpdated', ({ newTitle }) => {
  this.newTitle = newTitle
})

// or you can listen to ALL the events with a * wildcard
this.event_bus.on('*', (eventType, eventData) => {
  if (this.eventType === 'titleUpdated') {
    this.newTitle = eventData.newTitle
  }
})

Sounds great, does everything you need in such a small package (less than 200 bytes gzipped)!

Conclusion

If the official Event/Listener solution does not fit your needs because it's too limited, you can always roll out your own global event/listener solution. Turns out, it's super easy to do!

Remember that it's not an officially supported solution (after all, it's your custom code), but if you understand the risks and the way it works - you're good to go.

Need inspiration?

We’re here to help

Our documentation and blog articles cover everything you need to get started and more. But if you feel overwhelmed and need a helping hand, don't be shy and contact us directly at [email protected]