Vue.Js 3 Event Bus

Vue.js 3 Event Bus

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

How to setup a Global Event bus in Vue 3

  • First the error:

    did you try to export const bus = createApp(); in a different file to avoid circular ref ?

  • Regarding global bus in vue3:

    it is recommended to use a global state like what vuex provide you or any external package as best practice to communicate between components.

    and even if you managed to import the created bus you still don't have the listener $on to receive the emitted data. as they deleted it in vue3

Vue 3 Event Bus with Composition API

To use an event bus in Vue 3 Composition API, use Vue 3's new provide api in main.js, and then inject in any component:

1. Install mitt:

npm install mitt

2. Provide:

main.js

import { createApp } from 'vue';
import App from './App.vue';

import mitt from 'mitt'; // Import mitt
const emitter = mitt(); // Initialize mitt

const app = createApp(App);
app.provide('emitter', emitter); // ✅ Provide as `emitter`
app.mount('#app');

3. Inject

3a. Any Component - Emit an event

import { inject } from 'vue'

export default {
setup() {
const emitter = inject('emitter'); // Inject `emitter`
const mymethod = () => {
emitter.emit('myevent', 100);
};
return {
mymethod
}
}
}

Call mymethod from a button click or something.

3b. Any Component - Listen for the event

import { inject } from 'vue'

export default {
setup() {
const emitter = inject('emitter'); // Inject `emitter`

emitter.on('myevent', (value) => { // *Listen* for event
console.log('myevent received!', `value: ${value}`);
});
},
}

Console

myevent received! value: 100 

How to create a local event bus on Vue 3

  1. There is no event bus in Vue 3. So $on will not work. See: https://v3-migration.vuejs.org/breaking-changes/events-api.html#events-api

  2. It is recommended that you create your event bus with external libraries, unless you want to make use of the supported parent-children event-based communication in which the child emits an event which can only be listened to by the parent. See: https://v3.vuejs.org/guide/component-custom-events.html#custom-events

  3. To implement an event bus for your application, make sure that you are emitting (firing) the events and listening to them with the same instance of the emitter. Else, it won't work.

  4. I preferred emittery (https://github.com/sindresorhus/emittery) for event bus with Vue js. It is robust with excellent Typescript support. I also use it on Node js via the Adonisjs framework.

  5. Create a useEvent hook
    //file: composables/useEvent.ts

import Emittery from 'emittery';
const emitter = new Emittery();
// Export the Emittery class and its instance.
// The `emitter` instance is more important for us here
export {emitter, Emittery};

// Export the Emittery class and its instance
export { emitter, Emittery };


  1. In any vue component listen to an event
    // file: AnyVueComponent.vue
<script lang="ts">
import { defineComponent, onMounted } from 'vue';
import {emitter} from '../composables/useEvent'

export default defineComponent({
name: 'ExampleComponent',
components: {},
props: {},
setup() {
onMounted(() => {
emitter.on('event-name', async () => {
// Perform actions. async...await is supported
});
})
return {};
},
});
</script>

  1. In any vue component emit the event
<script lang="ts">
import { defineComponent, onMounted } from 'vue';
import {emitter} from '../composables/useEvent'

export default defineComponent({
name: 'ExampleComponent',
components: {},
props: {},
setup() {
void emitter.emit('event-name');
return {};
},
});
</script>

  1. Note that you can import the useEvent hook into any non-Vue file and it will work. It is just a JS/TS file.

For the Quasar Framework:

  1. Create a boot file
./node_modules/.bin/quasar new boot EventBus --format ts

  1. // file: EventBus.ts
import { boot } from 'quasar/wrappers';
import Emittery from 'emittery';

const emitter = new Emittery();

export default boot(({ app }) => {
app.config.globalProperties.$event = emitter;
});

// Export the Emittery class and its instance
export { emitter, Emittery };

  1. Register with the quasar.conf.js file
module.exports = configure(function(/* ctx */){
return {
boot: [
...,
'EventBus',
],
}
}

  1. In any vue component listen to an event
    // file: AnyVueComponent.vue
<script lang="ts">
import { defineComponent, onMounted } from 'vue';
import {emitter} from '../boot/EventBus'

export default defineComponent({
name: 'ExampleComponent',
components: {},
props: {},
setup() {
onMounted(() => {
emitter.on('event-name', async () => {
// Perform actions. async...await is supported
});
})
return {};
},
});
</script>

  1. In any vue component emit the event
<script lang="ts">
import { defineComponent, onMounted } from 'vue';
import {emitter} from '../boot/EventBus'

export default defineComponent({
name: 'ExampleComponent',
components: {},
props: {},
setup() {
void emitter.emit('event-name');
return {};
},
});
</script>

Unique EventBus for multiple instances of the same Vue application

In case anyone comes across this problem, I found a solution that works well for me. Thanks to @ChunbinLi for pointing me in the right direction - their solution did work, but passing props everywhere is a bit clunky.

Instead, I used the Provide/Inject pattern supported by Vue: https://v3.vuejs.org/guide/component-provide-inject.html

Some minimal relevant code:

The highest level Grandparent will provide the EventBus,

Grandparent.vue

export default {
provide() {
return {
eventBus: new Vue()
}
}
}

Then any descendant has the ability to Inject that bus,

Parent.vue

export default {
inject: ['eventBus'],
created() {
this.eventBus.$emit('neededEvent')
}
}

Child.vue

export default {
inject: ['eventBus'],
created(){
this.eventBus.$on('neededEvent', ()=>{console.log('Event triggered!')});
}
}

This is still a GLOBAL EventBus, so directionality of events and parental relationship is easy, as long as all components communicating are descendants of the component which "Provided" the bus.

Vue3 Sub-component pass data $on is not a function

I saw from the official website that $on, $off and $once methods are removed from vue 3.

https://v3-migration.vuejs.org/breaking-changes/events-api.html#_2-x-syntax

Vue 3 requires a third-party library to achieve.

index.js

import mitt from 'mitt'

const bus = mitt()
export default bus

App.vue

mounted() {
bus.on('maizuo', (data) => {
console.log(data)
})
}

Mitt event bus, not working inside axios interceptor but works fine in my vue components

app.config.globalProperties is used to create globals off of Vue component instances (not globals off window). You're incorrectly trying to use the global event bus in your Axios instance, as the this in that arrow function is undefined.

One solution is to factor out the event bus, and import it where needed:

// emitter.js
import mitt from 'mitt'
export default mitt()
// main.js
br>import emitter from './emitter.js'

const app = createApp(App) br>app.config.globalProperties.emitter = emitter
// client.js
br>import emitter from './emitter.js'

client.interceptors.response.use((response) => {
br> emitter.emit('alert', response.data);

return response;
}, (error) => {
return Promise.reject(error.message);
});


Related Topics



Leave a reply



Submit