Suggest an editImprove this articleRefine the answer for “Emits and custom events in Vue.js”. Your changes go to moderation before they’re published.Approval requiredContentWhat you’re changing🇺🇸EN🇺🇦UAPreviewTitle (EN)Short answer (EN)**Emits** let a child Vue component notify its parent by firing a custom event. The parent listens with `@eventName` and receives the payload as `$event`. ```vue <!-- Child.vue --> <script setup> const emit = defineEmits(['update']) const save = () => emit('update', { name: 'Alice' }) </script> <!-- Parent.vue --> <Child @update="handleUpdate" /> ``` **Key point:** always declare events with `defineEmits` - it enables TypeScript type checking and keeps Vue dev warnings clean.Shown above the full answer for quick recall.Answer (EN)Image**Emits** - child Vue components use them to send custom events up to parent components, reversing the direction of props. ## Theory ### TL;DR - A child component is like a submit button in a form: it does not know what the parent will do after the click, it just fires "submitted!" - Props flow down (parent controls data); emits flow up (child triggers notifications) - Use emits for child-to-parent communication; use Pinia for siblings; use `provide/inject` for deep component trees - `defineEmits` in `<script setup>` declares events and enables TypeScript validation - `v-model` on a component compiles to `:modelValue` + `@update:modelValue` under the hood ### Quick example ```vue <!-- Child.vue --> <script setup> const emit = defineEmits(['count-up']) const handleClick = () => emit('count-up', 1) </script> <template> <button @click="handleClick">+1</button> </template> ``` ```vue <!-- Parent.vue --> <script setup> import { ref } from 'vue' const total = ref(0) </script> <template> <Child @count-up="total += $event" /> <p>Total: {{ total }}</p> </template> ``` Click the button and `total` increments by 1 each time. The child never touches `total` directly. It fires a signal. The parent decides what to do with it. ### Props down, events up Props enforce one direction: parent sends data, child reads it. Emits go the other way. When a child needs the parent to act, it fires an event with a payload. The parent handles it. This keeps components decoupled - the same child component works with any parent that knows how to handle its events. ### When to use emits - Form submit in a child component, parent saves to API: emit `save` with form data - Modal close button, parent hides the overlay: emit `close` - Table row selection, parent updates selection state: emit `selection-change` - Two siblings need to share state: use Pinia, not emits - Deeply nested component needs to update root state: consider `provide/inject` or a store first ### TypeScript declarations In TypeScript projects, declare exact payload types with the generic syntax: ```vue <script setup lang="ts"> const emit = defineEmits<{ save: [profile: { name: string; avatar: string }] cancel: [] search: [query: string, page: number] }>() emit('save', { name: 'Alice', avatar: '/img.jpg' }) // type-checked emit('save', 'wrong') // type error emit('cancel') // no payload needed </script> ``` The IDE catches wrong argument types while you write, not at runtime. For simpler codebases, the array syntax `defineEmits(['save', 'cancel'])` still beats nothing. ### v-model under the hood `v-model` on a component compiles to a prop plus emit pair. The prop is `modelValue`, the emit is `update:modelValue`: ```vue <!-- CustomInput.vue --> <script setup> defineProps<{ modelValue: string }>() const emit = defineEmits<{ 'update:modelValue': [value: string] }>() </script> <template> <input :value="modelValue" @input="emit('update:modelValue', ($event.target as HTMLInputElement).value)" /> </template> ``` ```vue <!-- Parent.vue --> <template> <CustomInput v-model="username" /> <!-- same as: <CustomInput :modelValue="username" @update:modelValue="username = $event" /> --> </template> ``` Multiple `v-model` bindings on one component are possible too. Each needs its own named prop and a matching `update:propName` emit: ```vue <!-- UserForm.vue --> <script setup> defineProps<{ firstName: string; lastName: string }>() defineEmits<{ 'update:firstName': [value: string] 'update:lastName': [value: string] }>() </script> ``` ```vue <!-- Parent.vue --> <template> <UserForm v-model:firstName="first" v-model:lastName="last" /> </template> ``` ### How Vue processes emits When you call `emit('save', payload)`, Vue looks for a listener declared as `@save` on the component's vnode. It passes `payload` as `$event` to the handler. There is no DOM event here - this is Vue's own event system, not `addEventListener`. Because of that, emits don't bubble up the component tree. An event fired from a grandchild doesn't reach the grandparent unless you relay it manually or route it through a store. ### Common mistakes **Not declaring emits with defineEmits:** ```vue // Bad - Vue 3.3+ warns "Extraneous non-emits event listeners" const emit = () => {} // no defineEmits // Good const emit = defineEmits(['save', 'cancel']) ``` Without `defineEmits`, TypeScript validation is gone and dev warnings become noisy. **Mutating a prop directly in the child:** ```vue // Bad - Vue warns and the change reverts on next parent render props.user.name = 'Bob' // Good - emit the change, let the parent update emit('update:user', { ...props.user, name: 'Bob' }) ``` **Forgetting $event in an inline handler:** In my experience reviewing Vue code, this trips people up more than any other emit mistake. The event fires, the handler runs, but the payload disappears without a word. ```vue <!-- Bad - payload is silently lost --> <Child @save="console.log('saved')" /> <!-- Good - $event holds the payload --> <Child @save="handleSave($event)" /> <!-- or pass the function reference directly --> <Child @save="handleSave" /> ``` **Emitting in a loop:** ```vue // Bad - fires N separate events, each can trigger a re-render items.forEach(item => emit('add', item)) // Good - batch into one emit emit('add-batch', items) ``` **Using camelCase event names:** ```ts // Declared as camelCase defineEmits(['updateUser']) // Template convention is kebab-case // @update-user won't reliably match 'updateUser' in all cases ``` Stick to kebab-case or the `update:propName` pattern consistently. ### Real-world usage - Vuetify `v-text-field` emits `update:modelValue` on each keystroke for two-way binding - Element Plus `el-table` emits `selection-change` when the user selects rows - Nuxt page components use child modals that emit `close` to control overlay visibility - Custom form components emit `submit` with validated data to parent pages - Pinia integration: components emit `filter-update` which parent handlers pass to store actions ### Follow-up questions **Q:** What is the difference between `defineEmits` in `<script setup>` and `this.$emit` in Options API? **A:** `defineEmits` declares events at compile time and gives you TypeScript validation plus IDE autocomplete. `this.$emit` in Options API works at runtime only, with no type checking unless you add manual configuration on top. **Q:** Do Vue emits bubble up the component tree like DOM events? **A:** No. A Vue emit only reaches the direct parent. If a grandchild needs to notify the root, you relay the emit through each intermediate component or use a shared store. **Q:** How does Vue 3.4's `defineModel()` relate to emits? **A:** `defineModel()` is a compiler macro that automatically creates a prop plus emit pair. Under the hood it still emits `update:modelValue`, but it removes the boilerplate of writing both `defineProps` and `defineEmits` separately. **Q:** In SSR, do emits work the same way as on the client? **A:** Server-side rendering ignores emits - there is no DOM and no user interaction. Emits only matter on the client after hydration. If a parent handler has side effects that depend on being mounted, wrap them in `onMounted`. **Q:** Can a child emit an event to a grandparent without manual relay? **A:** Not with Vue emits directly. For ancestor communication use `provide/inject`. For anything crossing multiple component layers, a Pinia store is the cleaner option. ## Examples ### Basic: Counter button A reusable button that lets the parent decide what happens to the count: ```vue <!-- CounterButton.vue --> <script setup> const emit = defineEmits<{ increment: [amount: number] }>() </script> <template> <button @click="emit('increment', 1)">+1</button> <button @click="emit('increment', 10)">+10</button> </template> ``` ```vue <!-- App.vue --> <script setup> import { ref } from 'vue' import CounterButton from './CounterButton.vue' const count = ref(0) </script> <template> <CounterButton @increment="count += $event" /> <p>Count: {{ count }}</p> </template> ``` `CounterButton` holds no count state. It signals how much to add. The parent controls what that means. ### Intermediate: Profile editor An editable profile card where saves and cancellations go up to the dashboard: ```vue <!-- ProfileCard.vue --> <script setup lang="ts"> import { ref } from 'vue' const emit = defineEmits<{ save: [profile: { name: string; avatar: string }] cancel: [] }>() const profile = ref({ name: 'Alice', avatar: '' }) </script> <template> <div> <input v-model="profile.name" placeholder="Name" /> <input v-model="profile.avatar" placeholder="Avatar URL" /> <button @click="emit('save', profile)">Save</button> <button @click="emit('cancel')">Cancel</button> </div> </template> ``` ```vue <!-- Dashboard.vue --> <script setup lang="ts"> import { ref } from 'vue' import ProfileCard from './ProfileCard.vue' const profiles = ref<{ name: string; avatar: string }[]>([]) const handleSave = (newProfile: { name: string; avatar: string }) => { profiles.value.push(newProfile) } </script> <template> <ProfileCard @save="handleSave" @cancel="() => console.log('Cancelled')" /> <ul> <li v-for="p in profiles" :key="p.name">{{ p.name }}</li> </ul> </template> ``` Save appends to the list. Cancel logs a message. `ProfileCard` knows nothing about `profiles` - it reports what happened and moves on. ### Advanced: Password confirmation with multiple v-models Two inputs bound to different `v-model` props, each with its own emit: ```vue <!-- PasswordFields.vue --> <script setup lang="ts"> const props = defineProps<{ modelValue: string confirmValue: string }>() const emit = defineEmits<{ 'update:modelValue': [value: string] 'update:confirmValue': [value: string] }>() </script> <template> <div> <input :value="props.modelValue" placeholder="Password" type="password" @input="emit('update:modelValue', ($event.target as HTMLInputElement).value)" /> <input :value="props.confirmValue" placeholder="Confirm password" type="password" @input="emit('update:confirmValue', ($event.target as HTMLInputElement).value)" /> </div> </template> ``` ```vue <!-- SignupForm.vue --> <script setup lang="ts"> import { ref, watch } from 'vue' import PasswordFields from './PasswordFields.vue' const password = ref('') const confirm = ref('') watch([password, confirm], ([p, c]) => { if (p && c) console.log(p === c ? 'Passwords match' : 'No match') }) </script> <template> <PasswordFields v-model="password" v-model:confirmValue="confirm" /> </template> ``` Each field updates its own ref independently. The watcher catches mismatches as the user types. The tricky part here is the naming: `v-model:confirmValue` maps to the prop `confirmValue` and the emit `update:confirmValue`.For the reviewerNote to the moderator (optional)Visible only to the moderator. Helps review go faster.