Vue.js 3 EventBus: Streamlining Communication within Your Application

What is the updated way to implement Event Bus in Vue 3?


In previous versions of Vue (Vue 2), it was achieved through:

export const bus = new Vue();
bus.$on(...)
bus.$emit(...)

However, with the release of Vue 3, the Vue function no longer serves as a constructor. Instead, calling Vue.createApp({}) returns an object that does not have the $on and $emit methods.

Answer №1

Instead of relying on the traditional event bus in Vue, you can utilize the mitt library to dispatch events between components. Let's say we have a sidebar and a header that contains a button to open/close the sidebar, and we want that button to toggle a property inside the sidebar component:

In your main.js file, import the mitt library and create an emitter instance, defining it 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 the header component, 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 the sidebar component, 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>

If you are using the composition API, you can create a composable event bus like so:

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

Then, you can use the useEmitter function in your components just like you would with useRouter:

import useEmitter from '@/composables/useEmitter'

export default {
  setup() {
    const emitter = useEmitter()
    ...
  }
  ...
}

Using the composition API

You can take advantage of the new composition API to 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:

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) => {
  // destructure the parameters
    const [sidebarCollapsedBus] = val ?? []
    sidebarCollapsed.value = sidebarCollapsedBus
})

Answer №2

When working with version 3 of Vue.js, you have the option to incorporate a third-party library or utilize the features of the publisher-subscriber (PubSub) programming pattern.

eventHandler.js

//eventsHandler - a simple Javascript implementation of the publish subscribe pattern

class EventHandler{
    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 EventHandler();

main.js

import Vue from 'vue';
import $bus from '.../eventHandler.js';

const app = Vue.createApp({})
app.config.globalProperties.$bus = $bus;

Answer №3

Content of CommunicationChannel file:

class ChannelMessage extends Message {
  public content: any

  constructor({type, content} : {type: string, content: any}) {
    super(type)
    this.content = content
  }
}

class CommunicationChannel extends Channel {
  private static _instance: CommunicationChannel

  public static getInstance() : CommunicationChannel {
    if (!this._instance) this._instance = new CommunicationChannel()
    return this._instance
  }

  public send(type : string, content?: any) : void {
    this.sendMessage(new ChannelMessage({type, content}))
  }
}

export default CommunicationChannel.getInstance()

usage in project, send message:

import CommunicationChannel from '...path to communication channel file with class'
//...bla bla bla... code...
CommunicationChannel.send('message type', {..some content..}')

receive message:

import CommunicationChannel from '...path to communication channel file with class' 
//...bla bla bla... code...
CommunicationChannel.addEventListener('message type', (message) => { console.log(message.content) })

Answer №4

Let me share an interesting tidbit with you - you can also leverage the functionality of useEventBus provided by VueUse.

For instance, take a look at this TypeScript example that involves 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"
})

Answer №5

I have modified an existing solution to mimic the interface of a Vue instance, allowing for seamless integration as a substitute without altering the consuming code.

This updated iteration now includes support for the $off method, where the first argument can be an array of event names. It also resolves a previous issue in the $off method where unregistering multiple event listeners would inadvertently remove the wrong one due to forward iteration over the array while simultaneously removing elements from it.

event-bus.js:

// @ts-check

/**
 * A replacement for the traditional 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 })
  }

  /**
   * Refer to: https://v2.vuejs.org/v2/api/#vm-on
   *
   * @param {EventName} eventName
   * @param {Function} callback
   */
  $on(eventName, callback) {
    this.registerEventListener(eventName, callback)
  }
  
  // Additional methods omitted for brevity...

}

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'

Answer №6

Check out this enhanced version of Boussadjra Brahim's response utilizing the composition API. This method gives you the ability to trigger an event multiple times with or without a payload. Keep in mind that the counter is simply used to signal a change if no payload is provided, or if the same payload is triggered repeatedly.

It's important to exercise caution with SSR; in such cases, the bus variable will be shared, potentially causing state pollution across requests. An effective solution is to implement the event bus as a plugin and expose it through a composable. This ensures that the bus variable is initialized for each request.

utilizeEventBus.js

import { ref, watch } from 'vue';

const bus = ref(new Map());

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, callback) => {
    watch(() => bus.value.get(event), (val) => {
      callback(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>

Answer №7

It's interesting to note that the vue3 documentation doesn't mention it, but we have the ability to utilize javascript custom events on the window and catch the event in our desired vue3 component:

Imagine you wish to trigger a modal from any part of your vue3 project (App.vue) and your source component (App.vue) could contain:

<script setup>
function showMyCustomModal() {
  const showModal = new CustomEvent('modal::show', {
    // to hide, emit `modal::hide`, ensuring
    // the component is currently active and can respond to this event
    detail: 'my-custom-modal',
  })
  window.dispatchEvent(showModal);
}
</script>

Subsequently, your modal component (Modal.vue) can start monitoring that event when mounted:

<script setup>
  // define function to handle show modal event
  function handleModalShowEvent(event) {
    if (event.detail === props.id) {
      show();
    }
  }

  // define another for handling hide modal event
  function handleModalHideEvent(event) {
    if (event.detail === props.id) {
      hide();
    }
  }

  onMounted(() => {
    // when mounted, listen for the events
    window.addEventListener('modal::show', handleModalShowEvent);
    window.addEventListener('modal::hide', handleModalHideEvent);
  })

  onUnmounted(() => {
    // when unmounted, remove them:
    window.removeEventListener('modal::show', handleModalShowEvent);
    window.removeEventListener('modal::hide', handleModalHideEvent);
  })
</script>

Answer №8

When moving from Vue 2.x to Vue 3.0, transitioning to https://www.npmjs.com/package/vue-eventer involves only minimal code adjustments (simply initialization)...

// Vue 2.x
Vue.prototype.$eventBus = new Vue();

->

// Vue 3.x
import VueEventer from 'vue-eventer';
YourVueApp.config.globalProperties.$eventBus = new VueEventer();

Answer №9

Utilizing Vue composition along with defineEmit can simplify the process:

<!-- 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>

This demonstration features one child component, but you have the ability to pass the emit function down to additional children as well.

Answer №10

This solution utilizes the mitt library for event handling.

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

How to use in a project:
Component-emiter.js

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

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

Component-receiver.js

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

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

Similar questions

If you have not found the answer to your question or you are interested in this topic, then look at other similar questions below or use the search

Original: Custom/Modified 'Fibonacci' Number sequenceRevised: Personalized/Altered

I am looking to create a unique variation of the Fibonacci sequence. The implementation I have in mind is as follows: (FSM) - FSM(0) = 0, FSM(1) = 1, FSM(n) = FSM(n - 2) + FSM(n - 1) / n How can this be achieved using JavaScript? Specifically, I need to ...

Is there a way to pass a c struct pointer into javascript using ffi?

I need help passing a pointer to a struct to a method in nodejs using ffi. I am encountering an error where the type of the JavaScript struct I created cannot be determined. How can I resolve this issue? Previously, I was able to successfully implement si ...

Error occurs when attempting to access window.google in Next.js due to a TypeError

I've been working on integrating the Google Sign In feature into my Next app. Here's how I approached it. In _document.js import React from 'react'; import Document, {Html, Head, Main, NextScript } from 'next/document'; expo ...

Working with deeply nested objects in JavaScript

Given the following array structure: objNeeded = [ {onelevel: 'first'}, { onelevel: 'second', sublevels: [ {onelevel: 'domain'}, {onelevel: 'subdomain'} ] }, { ...

The functionality of the typeof operator is not behaving as anticipated

I am currently working on a script to verify the existence of a specific JavaScript object. var success = function(data) { var x= 0; var numOfCards = data.length; for (x=0;x<data.length - 1;x++) { if (typeof data[x].l ...

Tips for recursively updating a value with javascript functions

Given an array object listarray and passing updatevalue as a parameter, Note: The first larger value will be the first greater value than updatevalue Update the value of updatevalue to the first larger value, then increment the second larger value by add ...

Ways to update the state of an array without clearing the existing array items

I am attempting to add fetched array items to an existing state that already contains items (with plans to include pagination). However, when I try using code similar to setMovies((prevMovies) => [...prevMovies, ...arr1]), I encounter a Typescript erro ...

How can you use JavaScript regex to verify that the last three characters in a string are

What method can be used to confirm that a string concludes with precisely three digits? accepted examples: Hey-12-Therexx-111 001 xxx-5x444 rejected examples: Hey-12-Therexx-1111 Hey-12-Therexx-11 Hey-12-Therexx 12 112x ...

What is the best way to retain data after clicking a button?

I am facing an issue where I need to figure out how to add information to a new page when a button is clicked. For example, let's say I have an "add to cart" button and upon clicking it, I want to store some data. How can I achieve this functionality? ...

The ajax signal indicates success, yet there seems to be no update in the database

Hey there, thank you for taking the time to read this. Below is the code I'm currently working with: scripts/complete_backorder.php <?php if(!isset($_GET['order_id'])) { exit(); } else { $db = new PDO("CONNECTION INFO"); ...

Activate or deactivate a text box within a table based on the checkbox's status

I am seeking assistance with jQuery to enable or disable a textbox within a table when a checkbox is checked. Below is the code snippet from my edit.cshtml file. <table id="scDetails" class="dataTable"> <thead> & ...

I keep encountering an Uncaught TypeError when trying to read the property 'options' of null, despite having the element ID properly defined

I am a newcomer to the world of Javascript and HTML. Despite having the element defined in HTML, I am encountering an exception. Could someone please offer assistance? My goal is to create a shape (initially a circle) based on user input such as shape type ...

The header component in Vue does not display updated data before the router enters

I am facing an issue while trying to update data before entering a route in VueJs 2. In my component, I attempted the following: data: function () { return { name: "test" } }, beforeRouteEnter (to, from, next) { next(vm =& ...

PrimeVue (version 3.29.2) - Exploring the concept of `ThroughOptions`

I recently started incorporating PrimeVue into my Vue3 project and so far, I find it quite user-friendly. However, there is one particular aspect that's puzzling me: the concept of ThroughOptions. As I explored the API documentation of PrimeVue compo ...

Is there a way to display tooltip information for exactly 4 seconds when the page loads, hide it, and then have it reappear when hovered over

I am currently working with react-bootstrap version 0.32.4 and due to various changes, I am unable to update the version. My goal is to display a tooltip for 4 seconds upon page load and then hide it, only to show the tooltip again on hover. Here's ...

Confirming the information in two sections of a form

Could someone assist me with validating my form? Specifically, I need help ensuring that the house number field only accepts two numbers and the postcode field only allows 5 characters. I have implemented a pattern attribute for validation and I am curiou ...

Issue encountered while attempting to adjust a date (the modification was incorrect)

I am currently working on developing a calendar feature using Angular. Part of this project involves implementing drag and drop functionality to allow users to move appointments from one day to another. However, I have encountered a strange issue. When at ...

Creating an intricate table layout using AngularJS and the ngRepeat directive

I'm attempting to create a table similar to the one shown in the image below using HTML5. https://i.sstatic.net/DiPaa.png In this table, there is a multi-dimensional matrix with Class A and Class B highlighted in yellow. Each class has three modes ( ...

Changing Array Object into a different Array Object (with Angular)

I am dealing with an array Object [ { product: "Product A", inStock: 3, onHold: 1, soldOut: 2 }, { product: "Product B", inStock: 2, onHold: 0, soldOut: 1 }] I am struggling to convert it into the new array format below. Any assista ...

The process of combining identical data values within an array of objects

Can anyone help me with merging arrays in JavaScript? Here is an example array: [{ "team": team1, "groupname": "group1", "emp-data": [{ "id": 1, "name": "name1", }], }, { "team": team1, "groupname": "group1", " ...