Chapter 24: Vue $emit() Method
Vue: $emit() (and its modern Composition API version emit()).
This is the official way a child component talks back to its parent.
In Vue we have a very clear direction rule:
- Props go down (parent → child)
- Events go up (child → parent)
$emit() (or emit()) is how the child announces that “something happened” — and optionally sends data along with that announcement.
Why Do We Need $emit / emit?
Imagine these real situations:
- User clicks “Add to Cart” inside a ProductCard component → parent (shopping page) needs to know so it can update the cart count in the header
- User toggles a checkbox inside TodoItem → parent needs to mark that todo as done in the central list
- Form inside a modal emits “submit” → parent closes modal and saves data
- A dropdown inside a FilterPanel selects a category → parent filters the product list
Without events, the child would have to directly mutate parent’s data (very bad — breaks reactivity rules and makes debugging hell).
With $emit / emit → child stays dumb & reusable, parent stays in control.
Modern Way in Vue 3: defineEmits + emit() (2026 standard)
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 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 |
<!-- src/components/TodoItem.vue --> <template> <div class="todo-item" :class="{ completed: done }"> <input type="checkbox" :checked="done" @change="emitToggle" /> <span class="text" @dblclick="startEdit">{{ text }}</span> <!-- Show input only 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, computed, nextTick } from 'vue' const props = defineProps<{ id: number text: string done: boolean }>() const emit = defineEmits<{ (e: 'toggle', id: number): void (e: 'delete', id: number): void (e: 'update:text', id: number, newText: string): void // for editing }>() const isEditing = ref(false) const editText = ref(props.text) const editInput = ref<HTMLInputElement | null>(null) // Toggle checkbox function emitToggle() { emit('toggle', props.id) } // Delete button 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) { const newText = editText.value.trim() if (newText && newText !== props.text) { emit('update:text', props.id, newText) } 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: TodoList.vue (using v-for + listening to emits)
|
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 |
<template> <div class="todo-app"> <h1>Todos with Emits</h1> <div class="add-row"> <input v-model.trim="newTask" placeholder="Add new task..." @keyup.enter="addTask" /> <button @click="addTask">Add</button> </div> <TodoItem v-for="todo in todos" :key="todo.id" :id="todo.id" :text="todo.text" :done="todo.done" @toggle="toggleTodo" @delete="deleteTodo" @update:text="updateTodoText" /> <p class="stats">{{ 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 } ]) const newTask = ref('') function addTask() { if (!newTask.value.trim()) return todos.value.push({ id: Date.now(), text: newTask.value.trim(), done: false }) newTask.value = '' } 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> <style scoped> /* ... styles similar to previous examples ... */ .add-row { display: flex; gap: 0.8rem; margin-bottom: 1.5rem; } .stats { margin-top: 1.5rem; text-align: center; color: #4b5563; } </style> |
Key Takeaways – $emit / emit in 2026
| Feature | Old Options API | Modern <script setup> | Notes / Best Practice |
|---|---|---|---|
| Declare events | emits: [‘toggle’, ‘delete’] | defineEmits<…>() | Use TS types for safety |
| Emit event | this.$emit(‘toggle’, id) | emit(‘toggle’, id) | No this — much cleaner |
| With payload | this.$emit(‘update’, id, value) | emit(‘update:text’, id, newText) | Any number of args |
| 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 |
Quick Summary – When to Emit
| Child Action | Emit Name Convention | Parent Listens With | Payload Example |
|---|---|---|---|
| Checkbox toggled | toggle | @toggle | id |
| Item deleted | delete | @delete | id |
| Text edited | update:text or update | @update:text | id, newText |
| Form submitted | submit | @submit | form data object |
| Item selected (dropdown) | select / update:value | @select / @update:value | selected value |
Pro Tips from Real Projects
- 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
Want to go deeper?
- Make TodoItem support v-model:text?
- Show custom modal component that emits close and confirm?
- Explain $emit in Options API for legacy code?
- Events with multiple arguments or complex payloads?
Just tell me — we’ll keep building together 🚀
Happy emitting from Hyderabad! 💙
