In Vue 3, an event that isn’t declared in the component’s emits option isn’t treated as a component event: Vue instead attaches the
parent’s v-on listener directly to the component’s root element as a plain DOM listener.
This is most dangerous when the event name collides with a native DOM event, such as click, submit, or
change: the parent’s handler then fires twice, once from $emit() with the component’s own payload and once from the real DOM
event, and a handler written for the custom payload can throw on that second call.
Even a name that doesn’t collide with anything today isn’t safe: the DOM listener is still registered under that name and can start firing later, if the root element changes or a third party dispatches an event with the same name. In the common single-root case, nothing warns you when this happens, which is why this rule flags it.
Declaring the event also gives the type checker something to check the event name against: without a declaration, a typo like
this.$emit('svae') fails silently, with nothing warning you.
Add the event name to the component’s emit declaration. How you do this depends on the component’s API style: the emits option in the
Options API and in setup(), or defineEmits inside <script setup>, which is not available anywhere
else.
<script>
export default {
methods: {
onSubmit() {
this.$emit('submit'); // Noncompliant: "submit" is not declared in emits
}
}
}
</script>
<script>
export default {
emits: ['submit'],
methods: {
onSubmit() {
this.$emit('submit');
}
}
}
</script>
With Composition API’s explicit setup(props, { emit }), the event still needs to be declared in the component’s emits
option, not through defineEmits, since defineEmits only exists inside <script setup>. A quickfix is
available for both this and the Options API case above.
export default {
setup(props, { emit }) {
function onSubmit() {
emit('submit'); // Noncompliant: "submit" is not declared in emits
}
return { onSubmit };
}
}
export default {
emits: ['submit'],
setup(props, { emit }) {
function onSubmit() {
emit('submit');
}
return { onSubmit };
}
}
<script setup>, Composition API’s compile-time sugar, has no this, so $emit is not available in its
script block (it still works directly in template expressions). Declare your events with const emit = defineEmits([…]), and call them
through the emit function it returns. A quickfix can add a missing event name to an existing defineEmits array, but there is
no quickfix to create the declaration from scratch.
<script setup>
const emit = defineEmits(['submit']);
function onSubmit() {
emit('submit');
}
function onCancel() {
emit('cancel'); // Noncompliant: "cancel" is not declared in defineEmits
}
</script>
<script setup>
const emit = defineEmits(['submit', 'cancel']);
function onSubmit() {
emit('submit');
}
function onCancel() {
emit('cancel');
}
</script>
The standard form since Vue 3.3 is the tuple syntax, defineEmits<{ submit: [] }>(). Unlike the plain JavaScript array, adding an
event name here is not risk-free: writing cancel: [] for an event that actually carries a payload creates a new TypeScript error, so the
payload type has to match what is emitted. If the payload type is not known yet, or is not worth typing precisely, a fallback works too, from best to
worst: [payload: SomeType], [payload: unknown], or […args: any[]]. There is no quickfix for any type-based
declaration, and eslint-plugin-vue does not offer a suggestion for this form either.
<script setup lang="ts">
const emit = defineEmits<{ submit: [] }>();
function onSubmit() {
emit('submit');
}
function onCancel() {
emit('cancel'); // Noncompliant: "cancel" is not declared in defineEmits
}
</script>
<script setup lang="ts">
const emit = defineEmits<{ submit: []; cancel: [] }>();
function onSubmit() {
emit('submit');
}
function onCancel() {
emit('cancel');
}
</script>
Declaring an event whose name matches a native DOM event stops the parent’s listener from being attached as a native listener on the root element.
Before you declare such an event, make sure the component actually emits it on every interaction where consumers expect the native event to fire. This
also affects components that check $attrs directly, such as v-if="$attrs.onClose": they stop seeing the listener as soon as
it is declared.
For submit specifically, .prevent calls preventDefault() on whatever the child emits, so
this.$emit('submit') with no payload throws a TypeError once the event is declared. Emit the native event as the payload
(this.$emit('submit', event)) to avoid this.