skip to content

Emitted Events

defineEmits declares the events a component fires, optionally with validators, and parents listen with v-on. Interviewers probe why component events don't bubble and emits vs callback props.

part ofVue.jsoverview, primer and where to startread it →
on this pageshow

explore

questions

5

In a Vue 3 `<script setup>` component, how do you declare and emit a custom event, and how does the parent listen for it?

level: juniorimportance: must knowfreq 70%

answer

  1. a compile-time macro
  2. it returns a function
  3. extra arguments become the payload
  4. camelCase out, kebab-case in

basics

~10 s

Call defineEmits(['confirm']) at the top level of script setup, keep the returned emit function and call emit('confirm', id). The parent listens with v-on, @confirm="handler", and the handler receives the extra arguments.

solid answer

~30 s

In `<script setup>` you declare events with the `defineEmits()` macro, for example `const emit = defineEmits(['confirm', 'cancel'])`, placed directly at the top level, not inside a function. Calling `emit('confirm', item.id)` invokes the parent's listener, written with `v-on`: `<DeleteButton @confirm="removeItem" />`. Every argument after the event name is forwarded, so `removeItem` receives `item.id`. In the template you can also call `$emit('confirm')` directly. Names are case-transformed: a component emitting `deleteConfirmed` is heard by `@delete-confirmed`, which is the recommended listener casing in templates, and `.once` works on component listeners too.

code

vue · 19 lines
vue
<!-- DeleteButton.vue -->
<script setup lang="ts">
const props = defineProps<{ itemId: string }>()
const emit = defineEmits(['confirm', 'cancel'])

function onClick() {
  if (window.confirm('Delete this item?')) {
    emit('confirm', props.itemId)
  } else {
    emit('cancel')
  }
}
</script>

<template>
  <button type="button" class="danger" @click="onClick">Delete</button>
</template>

<!-- Parent: <DeleteButton :item-id="order.id" @confirm="removeOrder" @cancel="closeMenu" /> -->

go deeper

for a junior

Recall the trio: defineEmits declares, the returned emit function fires, and the parent listens with @event-name.

for a middle

Explain the macro's top-level rule, payload forwarding, .once, and why camelCase emits match kebab-case listeners except in in-DOM templates.

for a senior

Argue for declaring every event, since it documents outputs, keeps listeners out of fallthrough attributes and enables development warnings.

for a principal

Set event naming and payload conventions for a component library so consumers can predict names and shapes across components.

## What a component event is A Vue **component event** is a named signal a child sends to the component that rendered it. It is not a DOM event: nothing is dispatched on an element and nothing bubbles. Vue simply looks up the listener the parent attached to that child and calls it with the arguments you passed. ## Declaring events in script setup The `defineEmits()` macro declares the events a component can emit and returns an **emit function**: ```vue <script setup lang="ts"> const props = defineProps<{ itemId: string }>() const emit = defineEmits(['confirm', 'cancel']) function onYes() { emit('confirm', props.itemId) } </script> ``` Rules worth knowing: - `defineEmits()` is a **compile-time macro**; it needs no import and must sit directly at the top level of `<script setup>`, never inside a function or a condition. - The array form only lists names. The **object form** maps each name to `null` (no validation) or a validator function. - Outside `<script setup>`, the same declaration is the `emits` option, and `emit` comes from the `setup(props, ctx)` context. - In a template, `$emit('cancel')` is available without declaring a variable, but `$emit` is not accessible inside `<script setup>` code. ## Listening in the parent The parent uses `v-on` (the `@` shorthand) on the component tag: ```vue <DeleteButton :item-id="order.id" @confirm="removeOrder" @cancel="closeMenu" /> ``` - **Payload**: every argument after the event name is forwarded, so `emit('confirm', id, reason)` calls `removeOrder(id, reason)`. - **Inline handlers**: `@confirm="(id) => pending.add(id)"` works when you need the argument inline. - **`.once`**: `@confirm.once="track"` runs the handler only the first time the event is emitted. ## Naming and casing Vue transforms event names automatically: | Child emits | Parent writes | Result | |---|---|---| | `emit('deleteConfirmed')` | `@delete-confirmed` | handler runs | | `emit('deleteConfirmed')` | `@deleteConfirmed` | handler runs | | `emit('delete-confirmed')` | `@delete-confirmed` | handler runs | The compiler turns a component listener into a camelCase handler key (`onDeleteConfirmed`), and `emit()` tries both the exact and the camelized name. The documentation recommends **camelCase in `emit` calls and kebab-case in templates**, matching the convention for props. One exception: in **in-DOM templates** (markup written directly in an HTML page), the browser lowercases attribute names, so `@deleteConfirmed` arrives as `deleteconfirmed` and no longer matches; Vue logs a development warning suggesting the kebab-case form. ## Why declare at all Emitting works even for undeclared names, yet declaring is recommended: 1. It **documents** the component's outputs next to its props. 2. It keeps declared listeners out of the fallthrough attributes, so they are not attached to the root element as native listeners. 3. It enables **validation** with the object syntax, and type checking with TypeScript. 4. Once a component declares any events, emitting an undeclared name logs a development warning, which catches typos. ## Template $emit versus the emit function Both forms reach the same listener; they differ in where they can be used: | Form | Available in | Typical use | |---|---|---| | `$emit('cancel')` | the template only | one-line handlers such as `@click="$emit('cancel')"` | | `emit('confirm', id)` from `defineEmits` | `<script setup>` code and the template | logic that runs before emitting, such as a confirmation | Keeping all emits in script code makes the component's outputs easy to find, while template `$emit` keeps trivial pass-through handlers short. ## Common mistakes - **Emitting the DOM event.** `emit('confirm', $event)` hands the parent a `MouseEvent` it rarely wants; emit the domain value instead, such as the item id. - **Mutating a prop instead of emitting.** Props are readonly in the child; changing parent state is the parent's job, triggered by an event. - **Listening on the wrong component.** The listener must sit on the component that emits, because component events reach only the direct parent. - **Inconsistent names.** Mixing `deleted`, `onDelete` and `remove` across components makes templates hard to read; pick one verb per action. ## A worked flow A `<DeleteButton>` shows a confirmation, then calls `emit('confirm', props.itemId)`. The parent's `removeOrder(id)` runs synchronously inside that `emit` call. Nothing is returned to the child, and no ancestor above the parent hears the event unless the parent emits its own.

  • Why can defineEmits not be called inside a function in script setup?
    It is a compile-time macro: the compiler only transforms a call at the top level of `<script setup>` into the component's `emits` declaration. A call nested in a function is not compiled into that declaration, so it does not declare anything; the docs state it must be placed directly within `<script setup>`.
  • Does a Vue component listener written as @delete-confirmed hear emit('deleteConfirmed')?
    Yes. The compiler turns the listener into the handler key `onDeleteConfirmed`, and `emit()` looks up that camelized key. In an in-DOM template, though, the browser lowercases attribute names, so use kebab-case listeners there.
  • How is an emit payload with several values passed?
    Every argument after the event name is forwarded in order, so `emit('confirm', id, reason)` calls the handler with `(id, reason)`. Many teams prefer a single object payload so adding a field later does not reorder arguments.

saying these in an interview costs you the question

  • Component events bubble up to every ancestor like DOM events.
  • defineEmits must be imported from vue before use.
  • A kebab-case listener cannot hear an event emitted in camelCase.
  • You call this.$emit inside script setup to emit an event.
  • Only one payload argument can be passed with an emitted event.
open as a page

In Vue 3, a page listens for @deleted on `<OrderList>`, but only its nested `<DeleteButton>` emits deleted; why does the handler never run?

level: middleimportance: must knowfreq 55%

basics

~20 s

Vue'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.

open as a page

A Vue 3 `<DeleteButton>` emits click from its root `<button>`, and the parent's @click handler runs twice per click; why, and what fixes it?

level: middleimportance: should knowfreq 45%

basics

~20 s

An undeclared click listener falls through to the root button as a native listener, and the child's emit('click') calls the same handler again. Declaring click in defineEmits removes it from $attrs, so only the emitted event reaches it.

open as a page

A Vue 3 `<DeleteButton>` awaits a confirm dialog, then emits delete, but sometimes nothing happens and it cannot show a spinner; when should it take an onDelete callback prop instead?

level: seniorimportance: should knowfreq 35%

basics

~20 s

emit() returns nothing the child can await and is silently ignored once the component has unmounted. When the child needs the parent's result, such as a promise for a pending state, a function prop like onDelete fits better.

open as a page

In Vue 3, how does the object syntax of defineEmits validate an event payload, and what happens when validation fails?

level: middleimportance: nice to knowfreq 28%

basics

~20 s

With the object syntax, each event maps to null or a function that receives the emit arguments and returns a boolean. A false result logs a development warning, but the event is still delivered, and production builds skip validators.

open as a page