The event bus pattern in Vue 3

In Vue 2 any instance was an event emitter, so export const EventBus = new Vue() was a popular way to talk between components. Vue 3 removed $on, $off and $once; only $emit remains, for parent listeners.

Official guide: Events API, Vue 3 migration guide

Replace the bus with a tiny emitter

The quickest migration keeps the pattern and swaps the implementation for a small library such as mitt:

Vue 2
event-bus.js
// event-bus.js
import Vue from 'vue'
export const EventBus = new Vue()

// ProductList.vue
mounted() {
  EventBus.$on('cart:add', this.onAdd)
},
beforeDestroy() {
  EventBus.$off('cart:add', this.onAdd)
},

// AddButton.vue
EventBus.$emit('cart:add', product)
Vue 3
event-bus.js
// event-bus.js
import mitt from 'mitt'
export const emitter = mitt()

// ProductList.vue
mounted() {
  emitter.on('cart:add', this.onAdd)
},
beforeUnmount() {
  emitter.off('cart:add', this.onAdd)
},

// AddButton.vue
emitter.emit('cart:add', product)

Better long term options

  • Props and emits for parent and child communication.
  • provide and inject for a subtree that shares state.
  • Pinia (or Vuex 4) for application state that many components read and change.

Lifecycle hook events

this.$once('hook:beforeDestroy', fn) also relied on the instance emitter. In Vue 3, use the lifecycle hook directly, or @vue:mounted style listeners on child components in templates.

How vueleap detects it

vueleap reports each $on, $off and $once call (rule event-emitter, compat flag INSTANCE_EVENT_EMITTER), the new Vue() that creates the bus (rule global-mount), and hook: events (rule hook-events). Template @hook:mounted listeners are rewritten to @vue:mounted by --fix.

Run it on your project:

Terminalbash
npx vueleap