Updated June 2026. Tested on Vue 3.

In a real app you have components at many levels, and they need to talk to each other. In the Vue 2 days a popular trick was a global event bus, a spare Vue instance you used to emit and listen for events anywhere. If you are searching for that pattern, here is the important news: it is gone. Vue 3 removed $on, $off and $once from instances, so new Vue() as an event bus no longer works. That is a good thing, because there are clearer ways to do it. Here are the four you actually want, from simplest to most powerful.

1. Parent and child: props and emits

For a parent talking to a direct child, pass data down with props. For the child talking back up, emit an event. This covers most cases.

<!-- Child.vue -->
<script setup>
const emit = defineEmits(['saved'])

function save() {
  // do work, then tell the parent
  emit('saved', { id: 1 })
}
</script>
<!-- Parent.vue -->
<Child @saved="onSaved" />

2. Across deep levels: provide and inject

When data has to reach a component several levels down, threading props through every layer is painful. provide and inject let an ancestor offer a value that any descendant can pick up, no matter how deep.

<!-- Ancestor.vue -->
<script setup>
import { provide, ref } from 'vue'

const currentUser = ref(null)
provide('currentUser', currentUser)
</script>
<!-- some deep descendant -->
<script setup>
import { inject } from 'vue'

const currentUser = inject('currentUser')
</script>

Because you provide a ref, updates flow down reactively.

3. Unrelated components: a tiny bus with mitt

Sometimes two components with no parent and child link need to talk, which is the case the old event bus solved. The modern answer is a tiny library called mitt. It does the same job in a few hundred bytes.

npm install mitt
// eventBus.js
import mitt from 'mitt'
export const bus = mitt()
<!-- emitter -->
<script setup>
import { bus } from './eventBus'
bus.emit('logged-in', { id: 1 })
</script>
<!-- listener -->
<script setup>
import { onMounted, onUnmounted } from 'vue'
import { bus } from './eventBus'

function onLogin(user) { /* ... */ }

onMounted(() => bus.on('logged-in', onLogin))
onUnmounted(() => bus.off('logged-in', onLogin))
</script>

Always remove the listener in onUnmounted, the same care you took with the old bus, or you leak handlers.

4. Shared app state: Pinia

If components are really sharing state rather than firing one off events, a bus is the wrong tool. Reach for Pinia, the official Vue store. You define the state once and any component reads or changes it, and everything stays reactive.

// stores/user.js
import { defineStore } from 'pinia'
import { ref } from 'vue'

export const useUserStore = defineStore('user', () => {
  const current = ref(null)
  function setUser(u) { current.value = u }
  return { current, setUser }
})
<script setup>
import { useUserStore } from '@/stores/user'
const userStore = useUserStore()
// userStore.current is shared everywhere, reactively
</script>

Which one

  • Parent and child: props and emits.
  • Deep tree, one direction: provide and inject.
  • Unrelated components, occasional events: mitt.
  • Shared state across the app: Pinia.

Reach for the simplest one that fits. Most of the time props, emits and a store cover everything, and you rarely need a bus at all. Questions welcome in the comments.