106

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.

Boussadjra Brahim
  • 82,684
  • 19
  • 144
  • 164
KeyKi
  • 2,693
  • 3
  • 12
  • 24

8 Answers8

201

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
})

Boussadjra Brahim
  • 82,684
  • 19
  • 144
  • 164
  • As `app.config.globalProperties.` by default do not provide proper typing (and requires additional shims), using simple singleton approach might be a good (and simple) option: https://github.com/developit/mitt/issues/130#issuecomment-1135622062 With that you get simple, straight-forward minimal syntax, with proper typing. – Der Zinger Nov 09 '22 at 12:04
  • can you elaborate more? – Boussadjra Brahim Mar 03 '23 at 11:10
  • 4
    the composition based `useEventsBus()` is simple and beautiful! – Boern Mar 03 '23 at 11:16
  • 1
    for typescript Create a file named vue-shim.d.ts in your project's root directory. Add the following content to the vue-shim.d.ts file: import mitt, { Emitter } from 'mitt'; declare module '@vue/runtime-core' { interface ComponentCustomProperties { $emitter: Emitter; } } – BugsCreator Jun 20 '23 at 03:41
  • The watcher in the example of composition did not work for me but I am using Vue 3 markup in Vue 2 as I am preparing to upgrade soon. What did the job is to use `const bus = ref({})` and then use Vue.set() like this `Vue.set(bus.value, event, data)`. then the watcher started working again. – Julesezaar Aug 11 '23 at 08:05
  • for example of composition API, to make it trigger, value has to be different every time right? otherwise, the watcher will not work? is my understanding correct? was thinking to send constant value to other components. like sending `generate` string as value in event to know another component to generate data – muneebShabbir Aug 11 '23 at 08:16
51

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;
magistr4815
  • 640
  • 4
  • 10
8

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) })
4

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"
})
charlycou
  • 1,778
  • 13
  • 37
1

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'
kleinfreund
  • 6,546
  • 4
  • 30
  • 60
0

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();
mikep
  • 5,880
  • 2
  • 30
  • 37
0

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>
Towkir
  • 3,889
  • 2
  • 22
  • 41
-1

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.