Search⌘ K
AI Features

Create and Use Custom Events

Explore how to create and use custom events in VueJS to improve event management and communication between components. Learn to emit and listen to custom events, implement global event buses, and understand the differences between Vue and native JavaScript custom events. This lesson helps you build cleaner, more maintainable interactive applications by mastering advanced event handling techniques.

Events are the most fundamental concept of any interactive app. We know how to listen to many events in Vue, for example the v-on: and @... attributes, where v-on:click="someMethod" and @click="someMethod" are used to listen to click events and execute a component’s method respectively.

Sometimes, though, the standard events don’t entirely cover what we want to do. JavaScript offers custom events using CustomEvent, dispatchEvent, and addEventListener. We can find more info about them on the Mozilla Developer Network (MDN). Custom events work a bit differently in Vue, though.

Use cases and characteristics of custom events

There are several uses for custom events. We can use them to group larger happenings. A good example would be a custom form. Whenever a save event is triggered, the app should make an XHR request (XMLHttpRequest) to save the current state of the form. The save event can be triggered automatically every five minutes, for example, whenever the user presses the “save” button or each time there is debounced input.

Custom events make business logic agnostic from the user input. We can trigger a custom event at any time. In the form example above, several different triggers execute the saving. As a result, the saving becomes agnostic to user input.

We can also use custom events to build custom form controls. For example, a drag and drop library could emit a drag and a drop event that other components could listen to. Likewise, an article list component could emit a dataLoaded event to signal content being present.

Emitting custom events

We can emit a custom event via Vue’s $emit instance method. We call this.$emit('name of the event') whenever we want to emit an event. We can use the second parameter to pass any value around if we wish to add a payload, like a search string.

To listen to a custom event, we can use the normal @... or v-on:... syntax, like @dragged or v-on:data-loaded, and then execute whichever method we’d like. The parameter we’ll receive in the method is the payload only, not an event object, like with native events such as @click.

Let’s try this. In the following app, we’ve implemented a search-input component. Since searching is expensive and doesn’t offer the best performance, the search-input component should emit a search event with the user-entered string.

We should debounce the event, meaning it should be delayed, and the method will only executed if the user didn’t enter anything for one second. Another user input would start the waiting time anew. We can use debounce.js since it already has a debouncing function implemented.

<template>
  <div>
    <label>Search input</label>
    <input type="text" @input="emitSearch" v-model="search">
  </div>
</template>

<script>
export default {
  data() {
    return {
      search: ''
    }
  },

  methods: {
    emitSearch() {

    }
  }
}
</script>

A Vue app with a search-input component and a debounce utility function

Look at the hint below to find a solution for the challenge above:

Using an event bus

Sometimes, it’s necessary to emit events to other not directly related components. Think of a global loading spinner shown in the top navigation whenever content on the page is about to be loaded. We could pass things up until we’re at the basic App.vue and start to pass things down again with $refs and method calls, but this generates a large amount of overhead code. In addition, we might miss a place and need to debug many components to find where things went wrong.

Some Vue instance methods can help with this. By introducing an extra Vue instance that we can access globally, we can emit events in one place and listen to them somewhere else. To create an event bus, we have to first create a new Vue instance and make sure that any component importing it always gets the same instance:

Javascript (babel-node)
// eventBus.js
import Vue from 'vue'
const eventBus = new Vue({})
export default eventBus

By exporting the constant, we make sure to always export the same instance. Any other component could then emit custom events on the event bus with $emit:

HTML
<template>
<div>
<button @click="buttonClicked">
Click me!
</button>
</div>
</template>
<script>
import eventBus from '../eventBus.js'
export default {
methods: {
buttonClicked() {
eventBus.$emit('myCustomEvent')
}
}
}
</script>

And finally, any other component could listen to these events using $on. Since we attached the event listener on the global Vue instance, we need to make sure we remove it again if it’s not necessary anymore. It’s just like window.addEventListener. To remove a listener on the event bus, use $off.

HTML
<template>
<div></div>
</template>
<script>
import eventBus from '../eventBus.js'
export default {
mounted() {
eventBus.$on('myCustomEvent', this.doStuff.bind(this))
},
beforeDestroy() {
eventBus.$off('myCustomEvent', this.doStuff.bind(this))
},
method: {
doStuff(payload) {
// ...
}
}
}
</script>

However, this approach only works in Vue 2 because the global $emit and $on have been removed in Vue 3. For Vue 3, we need to implement a custom event bus or use an existing library. In the following examples, we’ll implement a custom event bus. To assign a global $emit and $on, we can use app.config.globalProperties, as illustrated in the example below:

Javascript (babel-node)
import { createApp } from 'vue'
import App from './App.vue'
const app = createApp(App)
class EventBus {
constructor () {
this.eventMap = new Map()
}
on(event, callback) {
if (!this.eventMap.has(event)) {
this.eventMap.set(event, [])
}
this.eventMap.set(event, [
...this.eventMap.get(event),
callback
])
}
emit (event, payload) {
if (this.eventMap.has(event)) {
this.eventMap.get(event).forEach(cb => cb(payload))
}
}
}
app.config.globalProperties.$bus = new EventBus()
/*
Usage:
this.$bus.on('myCustomEvent', payload => {
// ...
})
this.$bus.emit('myCustomEvent', 'somePayload')
*/
app.mount('#app')

In the following Vue app, we rebuild the global loading spinner example. First, we introduce a global event bus and add two global events—loading and loaded. Then, a click of the button in MyLoadingComponent should emit the loading event, wait for three seconds, and emit the loaded event.

The loading spinner should be aware that the app can load multiple things simultaneously. Therefore, it shouldn’t just turn off whenever it receives the first loaded event, and it should only stop when it receives just as many loaded events as loading events.

<template>
  <div>
    <span v-if="loading">
      Loading...
    </span>
    <span v-else>
      Loaded.
    </span>
  </div>
</template>

<script>
export default {
  data() {
    return {
      loading: false
    }
  },

  mounted() {
    // ...
  }
}
</script>

A Vue app with two components that should be made to interact with an event bus—a loading spinner and a loading component

The hint below shows how to implement this:

Event buses can be very useful, but they also have some downsides. First of all, they introduce abstraction. Business logic is happening at a distance without clearly indicating where we triggered an event. As with the loading spinner example, anything could trigger the spinner, and debugging that is hard.

A similar issue is namespacing. For example, a global event called loading can mean anything. What if we would like to differentiate between loading articles, loading user data, and loading comments? We can establish naming conventions, but they’re not enforced by the framework, making them hard to follow, especially for people who aren’t used to them.

The next problem we already mentioned in the introduction to event buses is manual cleanup. If we forget to do manual cleanups with $off in the beforeDestroy hook, we’ll introduce memory leaks that are very hard to debug.

It’s generally recommended to use state machines like Vuex instead of event buses. That might be why $on and $off got removed in Vue 3, making event buses impossible.

Vue’s custom events vs native JS custom events

Lastly, let’s have another look at native custom events in JS. In general, we can rebuild the $emit and v-on:... features using JavaScript’s CustomEvent class, like so:

HTML
<template>
<div>
<button @click="emitCustomEvent">
Click me!
</button>
</div>
</template>
<script>
export default {
methods: {
emitCustomEvent() {
const e = new CustomEvent('myCustomEvent')
this.$el.dispatchEvent(e)
}
}
}
</script>

We can then also listen to these events with standard JS methods and a reference in Vue.

<template>
  <div>
    <button @click="emitCustomEvent">
      Click me!
    </button>
  </div>
</template>

<script>
export default {
  methods: {
    emitCustomEvent() {
      const e = new CustomEvent('myCustomEvent')
      this.$el.dispatchEvent(e)
    }
  }
}
</script>

Listening to a native custom event in another component

Using native custom events would have some disadvantages, though. First of all, we need to do the manual cleanup, something that Vue would take over for us if we would use @... and $emit instead. Second, it’s unclear where the event listener is attached.

We could, in theory, put this in a mixin, but that would make it even more complex to debug. Furthermore, we can’t take advantage of Vue’s debugging tools since the event is no longer listed in the event chain. Lastly, the code is more complex and, therefore, harder to understand.