I needed to run certain actions at some common user interactions. These interactions could also be the same but in totally different parts of the application so required reusability. The easiest way I found to have a structured and ordered way to either notify about these interactions happening and react to them was using the Event Bus Pattern.
Using an Event Bus is an easy way to communicate different parts of our application. Events can be emitted from anywhere in our app (with or without payload) and then trigger some listener functions (which will receive the payload as parameter) that may be hooked to this particular event. Just like a Vue component emits an event, then its parent can capture and run some code in response. The event bus is essentially the same but in a more global scope without the parent-child structure.
Since the release of Vue 3, using a Vue instance as event bus is no longer the way to go. If we are looking to implement this pattern in our app we may require a library like Mitt
, which was recommended by vuejs.org itself (I can't find it anymore in the docs but I am pretty sure it was there).
In this article I want to show you how I have implemented this pattern in my Nuxt 3 applications using Mitt which already provides full TypeScript support. To fully understand what's happening under the hood I recommend to have a quick read of Mitt documentation before continue reading.
Mitt will act as the bus itself so all we have to do is just to create a plugin to be able to use its functionality in a more "Nuxt way". Let's create a plugin (plugins/event-bus.ts
) and basically map the functions we need from Mitt to our application.
I am assuming you have some knowledge about how Nuxt plugins and helpers providers work. Visit Automatically Providing Helpers for more info.
Mitt's API and Vue events are quite similar in naming ($emit/emit
and $on/on
). I have mapped these methods to event
and listen
(Laravel) to avoid potential collision and confusion.
import mitt from 'mitt'
export default defineNuxtPlugin(() => {
const emitter = mitt()
return {
provide: {
event: emitter.emit, // Will emit an event
listen: emitter.on // Will register a listener for an event
}
}
})
This will let us use $event
and $listen
functions anywhere in our application by just using the useNuxtApp
composable:
// components/register.vue
const { $event } = useNuxtApp()
const onUserRegistered = (user) => {
// User registration form was saved and then we want
// to notify the entire application about that.
$event('user:registered', user)
}
Then we can capture this event wherever we want:
// app.vue
// ideally you will have a plugin (or multiple) to register listeners
const { $event } = useNuxtApp()
$listen('user:registered', (user) => {
console.log('A user was registered!', user)
// do something here...
// An interesting example for this use case could be to
// just register an event or a conversion in your
// favourite analytics tool like Google Analytics, Fathom...
})
You can register multiple listeners for the same event anywhere in your application. Note that listeners can and must be removed at certain use cases. Just want to keep this example as simple as possible to refer to Mitt docs for more info.
That's all about how the event bus is supposed to work! But now we want to move one step further and since Mitt supports TypeScript, we can make sure we are firing safe event names with a safe payload too. Based on Mitt docs again we could improve our plugin like so:
import mitt from 'mitt'
interface User {
name: string
}
type ApplicationEvents = {
// translates to:
// user:registered -> event name
// User -> type as payload
'user:registered': User
'user:deleted': User
};
export default defineNuxtPlugin(() => {
// Then we just inform mitt about our event types
const emitter = mitt<ApplicationEvents>()
return {
provide: {
event: emitter.emit, // Will emit an event
listen: emitter.on // Will register a listener for an event
}
}
})
Voilà! Our editor is already aware about this and will suggest accordingly:
But not only that, the payload is also typed so our application will warn if we aren't providing the right type:
Now we are ready to use our Event Bus to fire any event we like and capture them for processing.
Based on @jacobandrewsk comments I will also show an example of how to use this as a composable instead of a plugin:
// composables/useEventBus.ts
import mitt from 'mitt'
type ApplicationEvents = {
'user:registered': User
};
const emitter = mitt<ApplicationEvents>()
export const useEvent = emitter.emit
export const useListen = emitter.on
// fire an event
useEvent('user:registered', { name: 'Israel'})
// capture
useListen('user:registered', (user) => console.log(user))
This way we can benefit from Nuxt 3 auto imports and use these two functions straight away anywhere in our application.
Conclusion
Here I have shown a very basic example but there is no limit on the amount of events your application can fire and react to.
I find this pattern specially useful for code reusability and making certain parts of my applications extensible. Integrating with third party services (like the example above) or displaying notifications could be a couple of examples. Feel free to use your imagination where this can be useful.
Leave a comment below and let me know what you think! 👇
One more thing
I am the co-founder of VueJobs –The #1 Vue.js Job Board– check it out if looking to Hire Vue.js Developers or Looking for Vue.js Jobs
Find me on Twitter: IsraelOrtuno
Top comments (12)
Really good article!
Just to make it bit more Vue/Nuxt 3 way, I think it would have been useful to wrap the logic from last chapters of the article into a composable like
useEmitEvent
anduseListenEvent
(names can be choosen better :D )But overall really nice one!
You could pass the name of the event as the first parameter and value as the second one. Thanks to that you could make the composable auto imported so that user would not have to import nuxtApp everytime :)
Jakub is on fire on Dev. 🔥🔥🔥
Added a composable example, thanks for the feedback
Build a nuxt module around
mitt
-Nuxt-Mitter
I would be grateful for any feedback or suggestions for improvement
npm-nuxt-mitter
Isn't this solved in a nicer way by state management plugins like pinia?
Not sure about Pinta making it that simple but for sure there are multiple ways to get similar behaviour. Even @atinux suggested using unjs/hookable twitter.com/Atinux/status/15946800...
thanks, will check that also
Thank you @israelortuno this is a great explanation. I've been searching for this answer for a couple days now and this simple, yet carefully explained example, is just money. Much appreciated.
Here is my composable solution with hooks.
gist.github.com/eserdinyo/5acf5924...
ref: twitter.com/Atinux/status/15946800...
Hi, this is great, how to write the type of emit and on please, thanks!
What's the difference with unjs/hookable used by nuxt?