In Vue 3, a page listens for @deleted on `<OrderList>`, but only its nested `<DeleteButton>` emits deleted; why does the handler never run?
answer
- emit looks at one vnode
- only the direct parent's listeners
- re-emit at each level
- deep trees need another channel
basics
~20 sVue's emit only calls listeners the direct parent placed on that component, and events never bubble. OrderList must listen to DeleteButton and emit its own deleted, or the page reaches the value through provide/inject or a shared store.
solid answer
~40 s`emit('deleted', id)` in `<DeleteButton>` looks up `onDeleted` only in the props **its own parent** passed to it, which is `<OrderList>`'s template. The page's `@deleted` sits on `<OrderList>`, so it is only called when `<OrderList>` itself emits `deleted`. Component events do not bubble. The standard fix is to **re-emit**: `<OrderList>` declares `deleted`, listens with `@deleted="(id) => emit('deleted', id)"`, and emits upward. For deep trees that relay becomes noise, so pass a function down with `provide`/`inject` or move the state to a store. A narrow case also works without code: if `<DeleteButton>` is `<OrderList>`'s single root and `<OrderList>` does not declare `deleted`, the page's listener falls through to it.
code
vue · 16 lines<!-- OrderList.vue -->
<script setup lang="ts">
defineProps<{ orders: { id: string; title: string }[] }>()
const emit = defineEmits(['deleted'])
</script>
<template>
<ul>
<li v-for="order in orders" :key="order.id">
{{ order.title }}
<DeleteButton :item-id="order.id" @deleted="(id: string) => emit('deleted', id)" />
</li>
</ul>
</template>
<!-- Page: <OrderList :orders="orders" @deleted="refresh" /> now runs refresh -->go deeper
Recall that a component event only reaches its direct parent's listener, so the middle component must emit its own event.
Explain that emit looks up on* handlers in its own vnode props, and show the re-emit pattern with a declared event.
Choose between relaying, provide/inject and a store by tree depth and coupling, and explain why an event bus is no longer idiomatic.
Set guidance on how deep an event may be relayed before a component library switches to injected actions or shared state.
## The setup The component tree looks like this: - the **page** renders `<OrderList @deleted="refresh" />` - `<OrderList>` renders a list of rows, each with a `<DeleteButton :item-id="order.id" />` - `<DeleteButton>` calls `emit('deleted', id)` after a successful delete `refresh` never runs. ## How emit finds a listener When a parent writes `@deleted="fn"` on a component tag, the compiler turns it into a prop-like entry, `onDeleted: fn`, on that **component's vnode**. `emit('deleted', id)` inside the component looks up `onDeleted` (and its camelCase variant) in **its own vnode's props** and calls it. That is the whole mechanism: 1. no DOM event is dispatched 2. no ancestor chain is walked 3. if the direct parent did not attach a listener, nothing happens and nothing warns The page's listener lives on `<OrderList>`'s vnode, not on `<DeleteButton>`'s, so `<DeleteButton>`'s emit cannot see it. The Vue docs state it plainly: component events do not bubble, and you can only listen to events emitted by a direct child. ## Option 1: re-emit at each level `<OrderList>` listens to its child and emits its own event: ```vue <script setup lang="ts"> const emit = defineEmits(['deleted']) </script> <template> <DeleteButton v-for="order in orders" :key="order.id" :item-id="order.id" @deleted="(id) => emit('deleted', id)" /> </template> ``` This keeps each component's **declared interface** honest: the page can see from `<OrderList>`'s `defineEmits` that it reports deletions. It is the right choice for one or two levels. ## Option 2: let the listener fall through A listener the middle component does **not** declare is a fallthrough attribute. If `<OrderList>` had a single root that was a `<DeleteButton>`, the page's `onDeleted` would be forwarded to it and would run. This rarely fits a list, and it silently stops working as soon as the middle component gains a wrapper element or declares the event, so treat it as a curiosity rather than a design. ## Option 3: a different channel for deep trees When the emitter sits many levels below the component that cares, relaying through every level adds boilerplate and couples components that do not care about the event. | Approach | Good for | Cost | |---|---|---| | re-emit per level | one or two levels | every level declares and forwards the event | | `provide` a callback, `inject` it below | a subtree that shares an action | the dependency is implicit in the tree | | a shared store | state many distant components read | a global dependency to design and test | Vue 2 projects often used a global **event bus** built on `$on`/`$emit`; the instance methods `$on`, `$off` and `$once` were removed in Vue 3, so that pattern now needs an external emitter library and is generally discouraged in favour of the options above. ## Naming the event at each level Re-emitting is also a chance to raise the level of abstraction. `<DeleteButton>` naturally says `deleted`, but `<OrderList>` can report something more meaningful to the page, such as `order-removed` with the whole order, or it can handle the deletion itself and emit nothing. Useful questions when designing the relay: 1. Does the page need to know **which** child acted, or only **that** something changed? 2. Should the middle component act on the event itself, for example removing the row optimistically, before reporting it? 3. Is the payload at this level the same shape the page wants, or should it be enriched? A relay that forwards the same name and payload through three levels is usually a sign that the state lives too far from the component that changes it, and that `provide`/`inject` or a store would fit better. ## Interview framing - State the mechanism, not just the rule: `emit` reads its own vnode's `on*` props. - Offer re-emitting as the default for shallow trees, because it keeps the contract visible. - Name `provide`/`inject` or a store for deep trees, and explain why an event bus is no longer the Vue 3 answer.
- Does Vue warn when a component emits an event that its parent is not listening to?No. Emitting without a listener is normal, since listeners are optional. Vue only warns in development when a component that declares emits fires a name that is neither declared nor an onX prop, which is about the child's own declaration, not about listeners.
- Why not simply use a global event bus in Vue 3?Vue 3 removed `$on`, `$off` and `$once`, so a bus needs an external emitter. Buses also hide which component talks to which, make cleanup on unmount your job, and are hard to trace. Re-emitting, provide/inject or a store keep the data flow visible.
saying these in an interview costs you the question
- Vue component events bubble to every ancestor that listens for the name.
- Adding .native or .capture to the page's listener makes it hear the nested emit.
- A listener on a parent component is inherited by every component it renders.
- Emitting without any listener throws an error in Vue 3.
- Vue 3 still supports $on for an event bus on the root instance.