Events — Messages Flowing Up
Each BookCard now shows its book, and you want a Remove button on it. But the card cannot change the list: the books array lives in App.vue, and props are read-only. The card needs a way to say "please remove me" and let the owner act. That is a component event.
The rule
The child emits a message up; the parent listens and decides.
The child declares the events it can send with defineEmits, another macro that needs no import, and sends one with emit:
<!-- BookCard.vue -->
<script setup>
const props = defineProps({ id: Number, title: String, read: Boolean })
const emit = defineEmits(['remove', 'toggleRead'])
</script>
<template>
<article class="card">
<h3>{{ title }}</h3>
<button @click="emit('toggleRead', id)">Mark read</button>
<button @click="emit('remove', id)">Remove</button>
</article>
</template>The first argument is the event name; anything after it is the payload, handed to the listener. The parent listens with @ (short for v-on), just like a click:
<!-- App.vue -->
<script setup>
import { ref } from 'vue'
import BookCard from './components/BookCard.vue'
const books = ref([
{ id: 1, title: 'Dune', read: true },
{ id: 2, title: 'Circe', read: false }
])
function removeBook(id) {
books.value = books.value.filter(b => b.id !== id)
}
function toggleRead(id) {
const book = books.value.find(b => b.id === id)
book.read = !book.read
}
</script>
<template>
<BookCard v-for="b in books" :key="b.id" v-bind="b"
@remove="removeBook" @toggle-read="toggleRead" />
</template>(v-for repeats the card once per book; the Directives section covers it.) Notice toggleRead in the child and @toggle-read in the parent. Vue matches camelCase emits with kebab-case listeners, the same way it does for props.
Two facts that surprise people:
- Component events do not bubble. A click on a button bubbles up through the DOM, but
emit('remove')reaches only the direct parent. If a grandparent needs it, the parent must listen and emit again. - Declaring emits matters. Events listed in
defineEmitsare treated as component events. An undeclared listener such as@removewould instead fall through to the card's root element as a native listener (you will see that in Attribute Inheritance).
The shortcut
Combine it with the props lesson into one loop: props down, events up, owner changes.
The child reports
The Remove button runs
emit('remove', 2). The card changes nothing itself.
Drills
Answer fast — trust the rule.
How does BookCard send a remove event carrying the book's id?
The child emits markFinished. How does the parent listen in its template?
BookCard sits inside BookList, which sits inside App. BookCard emits remove. Who hears it?
Why should the card emit remove instead of deleting the book from the list itself?
Remember
defineEmitsdeclares events;emit(name, payload)sends one; the parent uses@name.- Shortcut: props down, events up, owner changes.
- Trap: expecting component events to bubble past the direct parent.
