Emitting Custom Events
Vue's component model is built around one-way data flow: props travel down from parent to child, and events travel back up from child to parent. When a child component needs to notify its parent that something happened — a button was clicked, a form was submitted, a value changed — it does so by emitting a custom event. The parent then listens for that event using the v-on (@) directive, exactly as it would listen for a native DOM event like click. This pattern keeps components decoupled: a child never reaches into its parent's state directly, it simply announces what happened and lets the parent decide how to react.
Cricket analogy: Vue's one-way flow is like a captain sending fielding instructions down to a fielder, while the fielder only signals back 'catch taken' rather than repositioning the captain directly — the child announces what happened and the captain decides the next call.
Declaring emits with defineEmits
In <script setup>, you declare the events a component can emit using the defineEmits compiler macro. This is not strictly required to make emitting work, but it is strongly recommended because it documents the component's public event contract, enables IDE autocompletion, and lets Vue warn you in development if you emit an event that isn't declared. defineEmits returns an emit function you call with the event name and any payload arguments.
Cricket analogy: defineEmits is like a fielder formally telling the captain beforehand 'I only ever signal catch-taken or run-out appeal' — not required to field, but it documents the fielder's exact repertoire of signals so the whole team knows what to expect.
<script setup>
import { ref } from 'vue'
const emit = defineEmits(['update-quantity', 'remove-item'])
const quantity = ref(1)
function increment() {
quantity.value++
emit('update-quantity', quantity.value)
}
function removeFromCart() {
emit('remove-item')
}
</script>
<template>
<div class="cart-line">
<button @click="increment">+</button>
<span>{{ quantity }}</span>
<button @click="removeFromCart">Remove</button>
</div>
</template>Listening from the parent
The parent listens using v-on/@ with the exact event name declared by the child (event names are commonly kebab-case in templates). Any arguments passed to emit() become the arguments received by the handler. This mirrors how native DOM events work, which keeps the mental model consistent whether you're handling a click on a <button> or a custom update-quantity from a child component.
Cricket analogy: The captain listens for the fielder's exact call — 'catch-taken', in the lowercase style used on the field — and whatever details the fielder shouts, like which hand, become the arguments the captain's decision uses, just like reacting to any umpire signal.
<template>
<CartLine
v-for="item in items"
:key="item.id"
@update-quantity="onQuantityChange(item.id, $event)"
@remove-item="onRemove(item.id)"
/>
</template>Validating event payloads
Like props, emits can be declared with validation using the object syntax instead of an array. Each key maps to a validator function that returns true if the payload is valid, or false to trigger a console warning in development. This is especially useful for events that carry structured payloads, since it catches contract violations early during development rather than surfacing as subtle bugs downstream.
Cricket analogy: Validating an emit with a validator function is like a captain requiring that a fielder's 'catch-taken' signal include a valid fielder name and ball number, flagging a warning in the team huddle if the signal is malformed rather than letting a bad call slip through unnoticed.
const emit = defineEmits({
'update-quantity': (newQty) => {
return typeof newQty === 'number' && newQty >= 0
},
'remove-item': null // no validation needed
})In TypeScript projects, you can type emits with a generic type argument instead: defineEmits<{ 'update-quantity': [qty: number]; 'remove-item': [] }>(). This gives full compile-time checking of both event names and payload shapes, something Angular's @Output() EventEmitter<T> approximates but React has no direct equivalent for, since React relies on plain callback props instead of a dedicated event system.
A common pitfall is emitting an event name in camelCase (e.g. emit('updateQuantity', ...)) but listening for it in the parent template with the same camelCase (@updateQuantity). This actually works because Vue templates are case-insensitive for HTML attributes, but if you ever bind the listener dynamically via a render function or JSX, camelCase and kebab-case are NOT automatically reconciled — pick kebab-case for emitted event names to avoid surprises across both templates and render functions.
- Events flow up from child to parent, complementing props which flow down — together they form Vue's unidirectional data flow.
- Use
defineEmitsin<script setup>to declare and document a component's event contract, returning anemit()function. - Parents listen with
v-on/@eventName, and any extra arguments toemit()are passed through to the handler. - Emits can be validated using the object syntax, similar to prop validators, to catch malformed payloads in development.
- Prefer kebab-case event names in templates for consistency between template usage and render-function usage.
- Declaring emits (even though optional) improves IDE support and lets Vue warn about undeclared or misused events.
Practice what you learned
1. In `<script setup>`, what does `defineEmits` return?
2. Which directive does a parent use to listen for a custom event emitted by a child?
3. What is the benefit of declaring emits with the object syntax instead of an array?
4. Why is kebab-case recommended for custom event names?
5. What happens if a component emits an event that was not declared via defineEmits?
Was this page helpful?
You May Also Like
Props Explained
Explains how props pass data from parent to child components in Vue, covering declaration, validation, defaults, and the one-way data flow rule.
Defining and Registering Components
Covers how to author Single-File Components in Vue 3 and the difference between local and global component registration, with guidance on when to use each.
The setup() Function and <script setup>
Learn the entry point of the Composition API — the setup() function — and how <script setup> compiles away its boilerplate.
provide and inject
Understand Vue's dependency-injection mechanism for passing data deep through a component tree without prop drilling.
Related Reading
Related Study Notes in Web Development
Browse all study notesWebSockets Study Notes
Web Development · 30 topics
Web DevelopmentWebAssembly Study Notes
WebAssembly · 30 topics
Web DevelopmentgRPC Study Notes
Protocol Buffers · 30 topics
Web DevelopmentSpring Boot Study Notes
Java · 30 topics
Web DevelopmentFlask Study Notes
Python · 30 topics
Web DevelopmentDjango Study Notes
Python · 30 topics