127

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.

0

10 Answers 10

225

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

Sign up to request clarification or add additional context in comments.

7 Comments

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: github.com/developit/mitt/issues/130#issuecomment-1135622062 With that you get simple, straight-forward minimal syntax, with proper typing.
can you elaborate more?
the composition based useEventsBus() is simple and beautiful!
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; } }
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.
|
56

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;

Comments

11

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

Comments

11

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

Comments

6

Here is improved version of Boussadjra Brahim's answer with composition API. This approach allows you to trigger an event multiple times with payload or without it. Note, that counter is used just to trigger a change if no payload or payload is the same when you trigger same event several times.

Also be careful with SSR, in this case the bus variable will be shared, so you end up with cross-request state pollution. To avoid this you may implement event bus as a plugin and if needed expose it with composable. Like this the bus variable will be initialized for each request.

useEventBus.js

import { ref, watch } from 'vue';

export interface EventBus {
  emit: (event: string, props?: any) => void
  on: (event: string, cb: (props: any) => void) => void
  bus: Ref<Map<string, [any, number] | undefined>>
}

const bus = ref(new Map<string, [any, number] | undefined>());

export function useEventsBus() {
  const emit = (event, props) => {
    const currentValue = bus.value.get(event);
    const counter = currentValue ? ++currentValue[1] : 1;
    bus.value.set(event, [props, counter]);
  };

  const on = (event: string, cb: (props: any) => void) => {
    watch(
      () => bus.value.get(event),
      (val) => {
        if (val) {
          cb(val[0])
        }
      },
    )
  };

  return {
    emit,
    on,
    bus,
  };
}

PublisherComponent.vue

<script setup lang="ts">
import { useEventsBus } from '~/composables/useEventsBus';
</script>

<template>
  <Button
    @click="useEventsBus().emit('btn-clicked', 'Hello there')"
  >
    Button with payload
  </Button>
  <Button
    @click="useEventsBus().emit('btn-another-clicked')"
  >
    Button without payload
  </Button>
</template>

SubscriberComponent.vue

<script setup lang="ts">
import { useEventsBus } from '~/composables/useEventsBus';

useEventsBus().on('btn-clicked', (payload) => {
  console.log(payload); // 'Hello there'
});

useEventsBus().on('btn-another-clicked', (payload) => {
  console.log(payload); // undefined
})
// you can subscribe on the event several times
useEventsBus().on('btn-another-clicked', (payload) => {
  console.log(payload); // undefined
})
</script>

2 Comments

type for event, props and event, callback??
Updated: added types
5

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'

3 Comments

Your answer is missing the key information: how did you type EventBus so that you can access it?
Ready-mady NPM package for this (except array support) npmjs.com/package/vue-eventer
This one is great for me thanks!
4

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>

Comments

2

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();

Comments

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.

3 Comments

that works only Child-Parent way...
That does not work the other way around. Besides, the macro is named defineEmits() not defineEmit().
By definition, an event bus applies across any two components, there's no child/parent relationship.
1

Simple solution based on mitt library.

EventBus.js

import mitt from 'mitt'

const emitter = mitt()
export default {
    $on: (...args) => emitter.on(...args),
    $once: (...args) => emitter.once(...args),
    $off: (...args) => emitter.off(...args),
    $emit: (...args) => emitter.emit(...args)
}

Usage in project:
Component-emiter.js

import EventBus from "../../EventBus"; // path to EventBus.js

// ... code ...
EventBus.$emit('event name',{'some': 'data'});

Component-receiver.js

import EventBus from "../../EventBus"; // path to EventBus.js

// ... code ...
EventBus.$on('event name', (data) => {
    console.log('Event emitted', data);      
});

1 Comment

I like your solution but you need to rename in eventBus.js "emitter" to "EventBus". Else this is not going to work.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.