How to Use Vue Emits in Vue 3: Best Practices for Child-to-Parent Communication
Use defineEmits to explicitly declare events in <script setup>, emit with camelCase in JavaScript, listen with kebab-case in templates, and leverage runtime validators to catch payload errors during development.
Vue emits serve as the standard mechanism for child-to-parent communication in Vue 3, replacing the implicit event bus patterns of Vue 2 with a type-safe, compiler-assisted approach. According to the vuejs/core source code, the defineEmits macro and runtime validation system in packages/runtime-core/src/componentEmits.ts provide compile-time inference and dev-only warnings that prevent silent failures.
Declaring Vue Emits with defineEmits
The defineEmits macro is the primary way to declare events in the Composition API. It is a compile-time construct that disappears after build, leaving only the runtime emit function. In packages/runtime-core/src/apiSetupHelpers.ts (lines 134-148), the macro provides overloads for both array and object syntax.
Array Syntax for Simple Declarations
Use the array syntax when you only need to declare event names without payload validation:
<script setup lang="ts">
const emit = defineEmits(['save', 'cancel'])
function onSave() {
emit('save', { id: 42 })
}
function onCancel() {
emit('cancel')
}
</script>
Source: packages/runtime-core/src/apiSetupHelpers.ts lines 134-138.
Object Syntax with Validators
Use the object syntax when you need runtime payload validation. The validator function receives the emitted payload and should return a boolean indicating validity:
<script setup lang="ts">
const emit = defineEmits({
change: (value: number) => typeof value === 'number',
submit: (payload: { name: string }) => !!payload.name,
})
// Correct payload – passes validator
emit('change', 10)
// Incorrect payload – triggers dev warning
emit('change', 'oops')
</script>
Source: packages/runtime-core/src/apiSetupHelpers.ts lines 139-142.
Runtime Validation and Error Handling
The internal emit function in packages/runtime-core/src/componentEmits.ts (lines 24-55) performs several critical checks before invoking parent listeners. It validates the event against the component's emitsOptions, warns about undeclared events in development mode, and handles the once modifier by tracking emitted events in instance.emitted (lines 124-132).
When an event fails validation, Vue logs a helpful warning indicating the component name and the invalid payload, preventing silent failures that were common in Vue 2.
Naming Conventions: camelCase vs kebab-case
Vue normalizes event names to kebab-case when used in templates because HTML attributes are case-insensitive. The emit function contains a dev-only check (lines 76-89 in packages/runtime-core/src/componentEmits.ts) that warns when a camelCase event is emitted but the parent listens with a kebab-case name.
Best practice: Emit with camelCase in JavaScript and listen with kebab-case in templates.
<!-- ChildComponent.vue -->
<script setup>
const emit = defineEmits(['updateData'])
emit('updateData', newValue)
</script>
<!-- Parent.vue -->
<template>
<ChildComponent @update-data="handleUpdate" />
</template>
Handling v-model Modifiers Automatically
The internal emit function automatically detects update: events and applies number and trim modifiers (lines 61-69 in packages/runtime-core/src/componentEmits.ts). This means child components do not need manual modifier handling when implementing v-model.
<!-- Child -->
<script setup>
const emit = defineEmits(['update:modelValue'])
function onInput(e) {
emit('update:modelValue', e.target.value) // .number modifier handled automatically
}
</script>
<!-- Parent -->
<Child v-model.number="count" />
Options API: The emits Option and $emit
For Options API components, declare events using the emits option defined in packages/runtime-core/src/componentOptions.ts (line 161). Access the emit function via this.$emit, which is exposed on the public instance in packages/runtime-core/src/componentPublicInstance.ts (lines 315-317).
export default {
emits: ['update', 'delete'],
methods: {
requestUpdate(data) {
this.$emit('update', data)
},
requestDelete(id) {
this.$emit('delete', id) // Dev warning if event not declared
}
}
}
Performance Considerations
Vue caches normalized emit options using normalizeEmitsOptions and an internal emitsCache (referenced in packages/runtime-core/src/apiCreateApp.ts line 192). This ensures that components with many mixins or complex inheritance chains do not pay a normalization penalty on every instance.
Summary
- Always declare events using
defineEmitsin<script setup>or theemitsoption in Options API to enable dev-time validation and warnings. - Use object syntax with validators when you need runtime payload type checking.
- Emit camelCase, listen kebab-case to align with HTML attribute conventions and avoid dev warnings.
- Leverage automatic modifier handling for
v-modelby emittingupdate:modelValuewithout manual transformation logic. - Avoid
$emitin<script setup>; use the typedemitfunction returned bydefineEmitsinstead. - Trust the caching mechanism; Vue automatically optimizes emit option normalization across component instances.
Frequently Asked Questions
What happens if I emit an event that wasn't declared in defineEmits?
Vue will trigger a development-mode warning. The internal emit function in packages/runtime-core/src/componentEmits.ts checks the event name against the component's emitsOptions (lines 24-55) and logs a warning if the event is not registered, helping catch typos and API mismatches early.
Should I use this.$emit inside <script setup>?
No. Inside <script setup>, always use the emit function returned by defineEmits. The $emit property on the public instance (defined in packages/runtime-core/src/componentPublicInstance.ts lines 315-317) is intended for Options API components. Using the scoped emit ensures TypeScript correctly infers payload types.
How do I validate event payloads at runtime?
Use the object syntax in defineEmits and provide validator functions. Each key should be the event name, and the value should be a function that receives the payload and returns a boolean. According to the implementation in packages/runtime-core/src/componentEmits.ts (lines 40-48), if a validator returns false, Vue emits a development warning indicating the component and the invalid payload.
Do I need to manually handle v-model modifiers like .trim or .number?
No. When you emit an update:modelValue event (or any update: prefixed event), Vue's internal emit function automatically applies trim and number modifiers if the parent used them (as seen in packages/runtime-core/src/componentEmits.ts lines 61-69). Simply emit the raw value and let Vue handle the transformation.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →