Because when you re-emit a native event (like click) without declaring it in defineEmits, the parent's listener ends up attached in two places: once through $attrs fallthrough on the root element, and once through your explicit $emit() call.
vue
<!-- MyButton.vue — NO defineEmits -->
<template>
<button @click="$emit('click', $event)">
<slot />
</button>
</template>
<!-- Parent.vue -->
<MyButton @click="handleClick">Click me</MyButton>What happens on each click:
- Native click fires on the
<button> - Since
clickis not declared in emits,@clickfrom the parent falls through to the root element via$attrs, firinghandleClick - The
@click="$emit('click', $event)"also fires, emitting a component event that triggershandleClickagain
Result: handleClick runs twice.
How to fix it
Option 1: Declare the emit. This tells Vue that @click on the component is a component event, not a native one, so it won't fall through.
vue
<script setup>
const emit = defineEmits<{ click: [event: MouseEvent] }>()
</script>
<template>
<button @click="emit('click', $event)">
<slot />
</button>
</template>Option 2: Don't re-emit at all. If the component has a single root element, the native event falls through automatically.
vue
<!-- MyButton.vue — no emit needed, click falls through to <button> -->
<template>
<button>
<slot />
</button>
</template>
<!-- Parent.vue — works, fires once -->
<MyButton @click="handleClick">Click me</MyButton>The rule is simple: if you explicitly $emit a native event name, you must declare it in defineEmits. Otherwise the listener exists in two places.