Files
agent-skills/skills/vue-best-practices/reference/component-events-dont-bubble.md
Jason Woltje f5792c40be feat: Complete fleet — 94 skills across 10+ domains
Pulled ALL skills from 15 source repositories:
- anthropics/skills: 16 (docs, design, MCP, testing)
- obra/superpowers: 14 (TDD, debugging, agents, planning)
- coreyhaines31/marketingskills: 25 (marketing, CRO, SEO, growth)
- better-auth/skills: 5 (auth patterns)
- vercel-labs/agent-skills: 5 (React, design, Vercel)
- antfu/skills: 16 (Vue, Vite, Vitest, pnpm, Turborepo)
- Plus 13 individual skills from various repos

Mosaic Stack is not limited to coding — the Orchestrator and
subagents serve coding, business, design, marketing, writing,
logistics, analysis, and more.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-16 16:27:42 -06:00

5.5 KiB

title, impact, impactDescription, type, tags
title impact impactDescription type tags
Component Events Don't Bubble MEDIUM Vue component events only reach direct parent - sibling and grandparent communication requires alternative patterns gotcha
vue3
events
emit
event-bubbling
provide-inject
state-management

Component Events Don't Bubble

Impact: MEDIUM - Unlike native DOM events, Vue component events do NOT bubble up the component tree. When a child emits an event, only its direct parent can listen for it. Grandparent components and siblings never receive the event.

This is a common source of confusion for developers coming from vanilla JavaScript where events naturally bubble up the DOM.

Task Checklist

  • Only expect events from direct child components
  • Use provide/inject for deeply nested communication
  • Use state management (Pinia) for complex cross-component communication
  • Re-emit events at each level if manual bubbling is needed
  • Consider whether your component hierarchy is too deep

The Problem

<!-- GrandParent.vue -->
<template>
  <!-- This handler NEVER fires for grandchild events -->
  <Parent @item-selected="handleSelect" />
</template>
<!-- Parent.vue -->
<template>
  <Child />
</template>
<!-- Child.vue -->
<script setup>
const emit = defineEmits(['item-selected'])

function selectItem(item) {
  // This event reaches Parent, but NOT GrandParent
  emit('item-selected', item)
}
</script>

Solution 1: Re-emit at Each Level (Simple Cases)

Manually forward events through each component.

Correct:

<!-- GrandParent.vue -->
<template>
  <Parent @item-selected="handleSelect" />
</template>
<!-- Parent.vue -->
<script setup>
const emit = defineEmits(['item-selected'])
</script>

<template>
  <!-- Re-emit to grandparent -->
  <Child @item-selected="(item) => emit('item-selected', item)" />
</template>
<!-- Child.vue -->
<script setup>
const emit = defineEmits(['item-selected'])

function selectItem(item) {
  emit('item-selected', item)
}
</script>

Drawback: Becomes tedious with deeply nested components.

Solution 2: Provide/Inject (Ancestor Communication)

For deeply nested components, provide a callback from the ancestor.

Correct:

<!-- GrandParent.vue -->
<script setup>
import { provide } from 'vue'

function handleItemSelected(item) {
  console.log('Item selected:', item)
}

// Provide the callback to all descendants
provide('onItemSelected', handleItemSelected)
</script>

<template>
  <Parent />
</template>
<!-- Parent.vue - No changes needed -->
<template>
  <Child />
</template>
<!-- Child.vue -->
<script setup>
import { inject } from 'vue'

// Inject the callback from any ancestor
const onItemSelected = inject('onItemSelected', () => {})

function selectItem(item) {
  onItemSelected(item)
}
</script>

Advantages:

  • Skips intermediate components
  • No prop drilling or re-emitting
  • Works at any nesting depth

Solution 3: State Management (Complex Applications)

For cross-component communication, especially between siblings or unrelated components, use Pinia.

Correct:

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

export const useSelectionStore = defineStore('selection', () => {
  const selectedItem = ref(null)

  function selectItem(item) {
    selectedItem.value = item
  }

  return { selectedItem, selectItem }
})
<!-- DeepChild.vue - Updates state -->
<script setup>
import { useSelectionStore } from '@/stores/selection'

const store = useSelectionStore()

function handleSelect(item) {
  store.selectItem(item)
}
</script>
<!-- SiblingComponent.vue - Reacts to state -->
<script setup>
import { useSelectionStore } from '@/stores/selection'

const store = useSelectionStore()
</script>

<template>
  <div v-if="store.selectedItem">
    Selected: {{ store.selectedItem.name }}
  </div>
</template>

Solution 4: Event Bus (Use Sparingly)

For truly decoupled components, a simple event bus can work:

// eventBus.js
import mitt from 'mitt'
export const emitter = mitt()
<!-- ComponentA.vue -->
<script setup>
import { emitter } from './eventBus'

function notify() {
  emitter.emit('custom-event', { data: 'value' })
}
</script>
<!-- ComponentB.vue -->
<script setup>
import { onMounted, onUnmounted } from 'vue'
import { emitter } from './eventBus'

function handleEvent(data) {
  console.log('Received:', data)
}

onMounted(() => emitter.on('custom-event', handleEvent))
onUnmounted(() => emitter.off('custom-event', handleEvent))
</script>

Warning: Event buses make data flow hard to trace. Prefer provide/inject or state management.

Comparison Table

Method Best For Complexity
Re-emit 1-2 levels deep Low
Provide/Inject Deep nesting, ancestor communication Medium
Pinia/State Complex apps, sibling communication Medium
Event Bus Truly decoupled, rare cases Low (but risky)

Native Events DO Bubble

Note that native DOM events attached to elements still bubble normally:

<!-- GrandParent.vue -->
<template>
  <!-- Native click bubbles up from button inside Child -->
  <div @click="handleClick">
    <Parent />
  </div>
</template>

Only Vue component events (those emitted with emit()) don't bubble.

Reference