How to create Event Bus in Vue 3?
In Vue 2, it was:
export const bus = new Vue();
bus.$on(...)
bus.$emit(...)
In Vue 3, Vue is not a constructor anymore, and Vue.createApp({}); returns an object that has no $on and $emit methods.
How to create Event Bus in Vue 3?
In Vue 2, it was:
export const bus = new Vue();
bus.$on(...)
bus.$emit(...)
In Vue 3, Vue is not a constructor anymore, and Vue.createApp({}); returns an object that has no $on and $emit methods.
 
    
     
    
    As suggested in official docs you could use mitt library to dispatch events between components, let suppose that we have a sidebar and header which contains a button that close/open the sidebar and we need that button to toggle some property inside the sidebar component :
in main.js import that library and create an instance of that emitter and define as a global property:
Installation :
npm install --save mitt
Usage :
import { createApp } from 'vue'
import App from './App.vue'
import mitt from 'mitt';
const emitter = mitt();
const app = createApp(App);
app.config.globalProperties.emitter = emitter;
app.mount('#app');
in header emit the toggle-sidebar event with some payload :
<template>
  <header>
    <button @click="toggleSidebar"/>toggle</button>
  </header>
</template>
<script >
export default { 
  data() {
    return {
      sidebarOpen: true
    };
  },
  methods: {
    toggleSidebar() {
      this.sidebarOpen = !this.sidebarOpen;
      this.emitter.emit("toggle-sidebar", this.sidebarOpen);
    }
  }
};
</script>
In sidebar receive the event with the payload:
<template>
  <aside class="sidebar" :class="{'sidebar--toggled': !isOpen}">
  ....
  </aside>
</template>
<script>
export default {
  name: "sidebar",
  data() {
    return {
      isOpen: true
    };
  },
  mounted() { 
    this.emitter.on("toggle-sidebar", isOpen => {
      this.isOpen = isOpen;
    });
  }
};
</script>
For those using composition api they could use emitter as follows :
Create a file src/composables/useEmitter.js
import { getCurrentInstance } from 'vue'
export default function useEmitter() {
    const internalInstance = getCurrentInstance(); 
    const emitter = internalInstance.appContext.config.globalProperties.emitter;
    return emitter;
}
And from there on you can use useEmitter just like you would with useRouter:
import useEmitter from '@/composables/useEmitter'
export default {
  setup() {
    const emitter = useEmitter()
    ...
  }
  ...
}
Using the composition API
You could also take benefit from the new composition API and define a composable event bus :
eventBus.js
import { ref } from "vue";
const bus = ref(new Map());
export default function useEventsBus(){
    function emit(event, ...args) {
        bus.value.set(event, args);
    }
    return {
        emit,
        bus
    }
}
in component A do:
import useEventsBus from './eventBus';
...
//in script setup or inside the setup hook
const {emit}=useEventsBus()
...
 emit('sidebarCollapsed',val)
in component B :
const { bus } = useEventsBus()
watch(()=>bus.value.get('sidebarCollapsed'), (val) => {
  // destruct the parameters
    const [sidebarCollapsedBus] = val ?? []
    sidebarCollapsed.value = sidebarCollapsedBus
})
 
    
    On version 3 of Vue.js, you can use either a third-party library, or use the functionality written in the publisher-subscriber(PubSub concept) programming pattern.
event.js
//events - a super-basic Javascript (publish subscribe) pattern
class Event{
    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 (var i = 0; i < this.events[eventName].length; i++) {
                if (this.events[eventName][i] === fn) {
                    this.events[eventName].splice(i, 1);
                    break;
                }
            };
        }
    }
    trigger(eventName, data) {
        if (this.events[eventName]) {
            this.events[eventName].forEach(function(fn) {
                fn(data);
            });
        }
    }
}
export default new Event();
index.js
import Vue from 'vue';
import $bus from '.../event.js';
const app = Vue.createApp({})
app.config.globalProperties.$bus = $bus;
 
    
    Content of EventBus class file:
class EventBusEvent extends Event {
  public data: any
  constructor({type, data} : {type: string, data: any}) {
    super(type)
    this.data = data
  }
}
class EventBus extends EventTarget {
  private static _instance: EventBus
  public static getInstance() : EventBus {
    if (!this._instance) this._instance = new EventBus()
    return this._instance
  }
  public emit(type : string, data?: any) : void {
    this.dispatchEvent(new EventBusEvent({type, data}))
  }
}
export default EventBus.getInstance()
usage in project, emit event:
import EventBus from '...path to eventbus file with class'
//...bla bla bla... code...
EventBus.emit('event type', {..some data..}')
listen event:
import EventBus from '...path to eventbus file with class' 
//...bla bla bla... code...
EventBus.addEventListener('event type', (event) => { console.log(event.data) })
 
    
    I just want to mention here that you can also use useEventBus defined by VueUse.
Here is one example for TypeScript so using an injection key.
//myInjectionKey.ts
import type { EventBusKey } from '@vueuse/core'
export const myInjectionKey: EventBusKey<string> = Symbol('my-injection-key')
//emmitter
import { useEventBus } from '@vueuse/core'
import { myInjectionKey } from "src/config/myInjectionKey";
const bus = useEventBus(mapInjectionKey)
bus.emit("Hello")
//receiver
import { useEventBus } from '@vueuse/core'
import { myInjectionKey } from "src/config/myInjectionKey";
const bus = useEventBus(myInjectionKey)
bus.on((e) => {
    console.log(e) // "Hello"
})
 
    
    I’ve adapted another answer to have an equivalent interface to a Vue instance so that the utility works as a drop-in replacement that doesn’t require changes in the consuming code.
This version also supports the $off method with the first argument being an array of event names. It also avoids an issue in the $off method were de-registering multiple event listeners would actually delete a wrong one due to iterating over the array in forwards direction while also deleting items from it.
event-bus.js:
// @ts-check
/**
 * Replacement for the Vue 2-based EventBus.
 *
 * @template EventName
 */
class Bus {
  constructor() {
    /**
     * @type {Map<EventName, Array<{ callback: Function, once: boolean }>>}
     */
    this.eventListeners = new Map()
  }
  /**
   * @param {EventName} eventName
   * @param {Function} callback
   * @param {boolean} [once]
   * @private
   */
  registerEventListener(eventName, callback, once = false) {
    if (!this.eventListeners.has(eventName)) {
      this.eventListeners.set(eventName, [])
    }
    const eventListeners = this.eventListeners.get(eventName)
    eventListeners.push({ callback, once })
  }
  /**
   * See: https://v2.vuejs.org/v2/api/#vm-on
   *
   * @param {EventName} eventName
   * @param {Function} callback
   */
  $on(eventName, callback) {
    this.registerEventListener(eventName, callback)
  }
  /**
   * See: https://v2.vuejs.org/v2/api/#vm-once
   *
   * @param {EventName} eventName
   * @param {Function} callback
   */
  $once(eventName, callback) {
    const once = true
    this.registerEventListener(eventName, callback, once)
  }
  /**
   * Removes all event listeners for the given event name or names.
   *
   * When provided with a callback function, removes only event listeners matching the provided function.
   *
   * See: https://v2.vuejs.org/v2/api/#vm-off
   *
   * @param {EventName | EventName[]} eventNameOrNames
   * @param {Function} [callback]
   */
  $off(eventNameOrNames, callback = undefined) {
    const eventNames = Array.isArray(eventNameOrNames) ? eventNameOrNames : [eventNameOrNames]
    for (const eventName of eventNames) {
      const eventListeners = this.eventListeners.get(eventName)
      if (eventListeners === undefined) {
        continue
      }
      if (typeof callback === 'function') {
        for (let i = eventListeners.length - 1; i >= 0; i--) {
          if (eventListeners[i].callback === callback) {
            eventListeners.splice(i, 1)
          }
        }
      } else {
        this.eventListeners.delete(eventName)
      }
    }
  }
  /**
   * See: https://v2.vuejs.org/v2/api/#vm-emit
   *
   * @param {EventName} eventName
   * @param {any} args
   */
  $emit(eventName, ...args) {
    if (!this.eventListeners.has(eventName)) {
      return
    }
    const eventListeners = this.eventListeners.get(eventName)
    const eventListenerIndexesToDelete = []
    for (const [eventListenerIndex, eventListener] of eventListeners.entries()) {
      eventListener.callback(...args)
      if (eventListener.once) {
        eventListenerIndexesToDelete.push(eventListenerIndex)
      }
    }
    for (let i = eventListenerIndexesToDelete.length - 1; i >= 0; i--) {
      eventListeners.splice(eventListenerIndexesToDelete[i], 1)
    }
  }
}
const EventBus = new Bus()
export default EventBus
old-event-bus.js:
import Vue from 'vue'
const EventBus = new Vue()
export default EventBus
example.js:
// import EventBus from './old-event-bus.js'
import EventBus from './event-bus.js'
 
    
    Using https://www.npmjs.com/package/vue-eventer means minimal code changes when migrating from Vue 2.x to Vue 3.0 (just initialization)...
// Vue 2.x
Vue.prototype.$eventBus = new Vue();
->
// Vue 3.x
import VueEventer from 'vue-eventer';
YourVueApp.config.globalProperties.$eventBus = new VueEventer();
 
    
    Not sure why vue3 documentation doesn't hint this but we can use javascript custom events on the window and capture the event in our desired vue3 component:
Let's consider you want to be able to trigger a modal from anywhere in your vue3 project (App.vue) and your source component (App.vue) can have:
<script setup>
function showMyCustomModal() {
  const showModal = new CustomEvent('modal::show', {
    // for hiding, send `modal::hide`, just make sure
    // the component is currently mounted and it can listen to this event
    detail: 'my-custom-modal',
  })
  window.dispatchEvent(showModal);
}
</script>
And your modal component (Modal.vue) can start listening to that event whenever mounted:
<script setup>
  // define function for handling show modal event
  function handleModalShowEvent(event) {
    if (event.detail === props.id) {
      show();
    }
  }
  // define another for handling hide modal
  function handleModalHideEvent(event) {
    if (event.detail === props.id) {
      hide();
    }
  }
  onMounted(() => {
    // on mounted, listen to the events
    window.addEventListener('modal::show', handleModalShowEvent);
    window.addEventListener('modal::hide', handleModalHideEvent);
  })
  onUnmounted(() => {
    // on unmounted, remove them:
    window.removeEventListener('modal::show', handleModalShowEvent);
    window.removeEventListener('modal::hide', handleModalHideEvent);
  })
</script>
 
    
    With Vue composition and defineEmit you can even make it easier :
<!-- Parent -->
<script setup>
  import { defineEmit } from 'vue'
  const emit = defineEmit(['selected'])
  const onEmit = (data) => console.log(data)
</script>
<template>
    <btnList
        v-for="x in y"
        :key="x"
        :emit="emit"
        @selected="onEmit"
    />
</template>
<!-- Children (BtnList.vue) -->
<script setup>
  import { defineProps } from 'vue'
  const props = defineProps({
      emit: Function
  })
</script>
<template>
    <button v-for="x in 10" :key="x" @click="props.emit('selected', x)">Click {{ x }}</button>
</template>
I just showed it with one children, but you could pass though the emit function down to other children.
