Chapter 66: Vue $emit() Method
The $emit() method (in modern Vue 3 with <script setup> it’s usually just called emit())
This is the official, recommended way a child component tells its parent that “something happened”.
In Vue we follow a very clear direction rule (one of the first things you should burn into your brain):
- Props go down → parent → child (data flows downward)
- Events go up → child → parent (actions / notifications flow upward)
$emit() / emit() is how the child announces an event to the parent — and optionally sends data (payload) along with that announcement.
Without events, the child would have to directly mutate parent’s data (very bad — breaks reactivity rules and makes debugging hell).
With events → child stays dumb & reusable, parent stays in control.
Modern Vue 3 Way: defineEmits + emit() (what you should use in 2026)
In <script setup> we never use this.$emit anymore — we use:
|
0 1 2 3 4 5 6 7 |
const emit = defineEmits<...>() emit('event-name', payload?) |
Real, Complete Example – TodoItem that Emits Multiple Events
Child: TodoItem.vue
|
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 |
<template> <div class="todo-item" :class="{ completed: done }"> <input type="checkbox" :checked="done" @change="emitToggle" /> <span class="text" @dblclick="startEdit">{{ text }}</span> <!-- Inline edit input when editing --> <input v-if="isEditing" v-model="editText" @blur="finishEdit" @keyup.enter="finishEdit" @keyup.esc="cancelEdit" class="edit-input" ref="editInput" /> <button class="delete-btn" @click="emitDelete"> × </button> </div> </template> <script setup lang="ts"> import { ref, nextTick } from 'vue' const props = defineProps<{ id: number text: string done: boolean }>() // 1. Declare all possible events with TypeScript (best practice 2026) const emit = defineEmits<{ (e: 'toggle', id: number): void (e: 'delete', id: number): void (e: 'update', id: number, newText: string): void // for editing }>() const isEditing = ref(false) const editText = ref(props.text) const editInput = ref<HTMLInputElement | null>(null) // ── Event emitters ──────────────────────────────────────── function emitToggle() { emit('toggle', props.id) } function emitDelete() { emit('delete', props.id) } // ── Edit flow ───────────────────────────────────────────── function startEdit() { isEditing.value = true editText.value = props.text nextTick(() => { editInput.value?.focus() editInput.value?.select() }) } function finishEdit() { if (!isEditing.value) return const newText = editText.value.trim() if (newText && newText !== props.text) { emit('update', props.id, newText) // ← emit update event } isEditing.value = false } function cancelEdit() { isEditing.value = false editText.value = props.text } </script> <style scoped> .todo-item { display: flex; align-items: center; padding: 0.9rem 1.2rem; background: white; border-radius: 8px; margin-bottom: 0.6rem; box-shadow: 0 1px 4px rgba(0,0,0,0.08); } .completed .text { text-decoration: line-through; color: #9ca3af; } .text { flex: 1; margin: 0 1rem; cursor: pointer; font-size: 1.05rem; } .edit-input { flex: 1; margin: 0 1rem; padding: 0.4rem 0.8rem; font-size: 1.05rem; border: 1px solid #3b82f6; border-radius: 4px; } .delete-btn { background: none; border: none; color: #ef4444; font-size: 1.5rem; cursor: pointer; padding: 0 0.6rem; opacity: 0.7; } .delete-btn:hover { opacity: 1; } </style> |
Parent – listens to all emitted events
|
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 |
<template> <div class="todo-list"> <h2>My Todos</h2> <TodoItem v-for="todo in todos" :key="todo.id" :id="todo.id" :text="todo.text" :done="todo.done" @toggle="toggleTodo" @delete="deleteTodo" @update="updateTodoText" /> <p>{{ remaining }} remaining</p> </div> </template> <script setup lang="ts"> import { ref, computed } from 'vue' import TodoItem from '@/components/TodoItem.vue' interface Todo { id: number text: string done: boolean } const todos = ref<Todo[]>([ { id: 1, text: 'Learn emit()', done: true }, { id: 2, text: 'Build todo with edit', done: false } ]) function toggleTodo(id: number) { const todo = todos.value.find(t => t.id === id) if (todo) todo.done = !todo.done } function deleteTodo(id: number) { todos.value = todos.value.filter(t => t.id !== id) } function updateTodoText(id: number, newText: string) { const todo = todos.value.find(t => t.id === id) if (todo) todo.text = newText } const remaining = computed(() => todos.value.filter(t => !t.done).length) </script> |
Key Takeaways – emit() in 2026
| Feature | Old Options API | Modern <script setup> | Best Practice Tip |
|---|---|---|---|
| Declare events | emits: [‘toggle’, ‘delete’] | defineEmits<…>() | Always use TypeScript types |
| Emit event | this.$emit(‘toggle’, id) | emit(‘toggle’, id) | No this — much cleaner |
| With payload | this.$emit(‘update’, id, value) | emit(‘update’, id, newText) | Any number of arguments |
| v-model support | this.$emit(‘update:value’, val) | emit(‘update:modelValue’, val) | Enables v-model on component |
| Event name convention | kebab-case (update:user) | kebab-case in template, camel in JS | Auto-converted |
| Multiple events | emits: [‘click’, ‘focus’] | defineEmits([‘click’, ‘focus’]) | List all possible events |
Quick Summary – When to Emit What
| Child Action | Typical Event Name | Payload Example | Parent Listens With |
|---|---|---|---|
| Checkbox toggled | toggle / update:done | id or { id, done } | @toggle / @update:done |
| Item deleted | delete / remove | id | @delete |
| Text edited | update / update:text | id, newText | @update:text |
| Form submitted | submit | form data object | @submit |
| Item selected (dropdown) | select / update:value | selected value / object | @select / @update:value |
| Modal closed | close / update:modelValue | false | @close / v-model |
Pro Tips from Real Projects (Hyderabad 2026)
- Always declare events with defineEmits — gives autocompletion & catches typos
- Use kebab-case for event names in templates (@update:text)
- For two-way binding → use update:propName pattern → enables v-model
- Keep payloads small & serializable (numbers, strings, plain objects — avoid functions, DOM nodes)
- Child should never mutate props — always emit → parent updates
- In UI libraries → emit standard names: update:modelValue, close, confirm, cancel, select, change
Your mini homework:
- Create TodoItem.vue exactly as above
- Use it in parent with all three events
- Try to mutate a prop inside child → see Vue warning in console
- Add v-model:text support on TodoItem (emit update:text)
Any part confusing? Want full examples for:
- Custom component with multiple v-model (v-model:title, v-model:body)?
- Emit with complex payload (object, array)?
- $emit in Options API for legacy code?
- Events with TypeScript typing + validation?
Just tell me — we’ll build the next clean event-based component together 🚀
