Components: Props and Emits
Introduction
Components split UI into reusable pieces. Props pass data down from parent to child; emits send events up. Vue enforces one-way data flow—children should not mutate props directly. This chapter covers defineProps, defineEmits, validation, and naming conventions in <script setup>.
Prerequisites
Basic Props
UserCard.vue:
Parent App.vue:
<script setup lang="ts">
import UserCard from "./components/UserCard.vue";
</script>
<template>
<UserCard name="Ada Lo" email="ada@example.com" :active="true" />
</template>Code explanation:
:active="true"passes boolean;active="true"passes string"true"- Optional props use
?in TypeScript
Default Values with withDefaults
<script setup lang="ts">
withDefaults(
defineProps<{
title: string;
size?: "sm" | "md" | "lg";
}>(),
{
size: "md",
}
);
</script>Runtime declaration alternative (JavaScript):
<script setup>
defineProps({
title: { type: String, required: true },
size: { type: String, default: "md" },
});
</script>TypeScript + defineProps is preferred in this track.
Prop Naming: camelCase vs kebab-case
Script uses camelCase; HTML attributes are case-insensitive—use kebab-case in templates:
<!-- Child -->
<script setup lang="ts">
defineProps<{ postCount: number }>();
</script>
<!-- Parent -->
<ProfileCard :post-count="42" />One-Way Data Flow
Do not assign to props:
<!-- Wrong -->
<script setup lang="ts">
const props = defineProps<{ count: number }>();
props.count += 1; // error / anti-pattern
</script>Copy to local ref if child needs editable draft:
Or emit updates to parent—preferred for shared truth.
defineEmits
SaveButton.vue:
Parent listens with @ (same as native events):
<SaveButton @save="handleSave" @cancel="handleCancel" />Emit with Payload
<script setup lang="ts">
type Todo = { id: number; text: string };
const emit = defineEmits<{
update: [todo: Todo];
remove: [id: number];
}>();
function save(todo: Todo) {
emit("update", todo);
}
</script>Parent:
<TodoRow @update="onUpdate" @remove="onRemove" />function onUpdate(todo: Todo) {
// merge into list
}
function onRemove(id: number) {
// filter list
}validate Emits (Optional)
Runtime names array:
<script setup lang="ts">
const emit = defineEmits(["save", "cancel"]);
</script>Typed emits catch typos at compile time—prefer generic form.
Props + Emits Together: Controlled Input
Or use defineModel—Forms and v-model.
Global vs Local Registration
This track uses local imports:
import UserCard from "./components/UserCard.vue";Avoid app.component() global registration except plugins—harder to trace dependencies.
Component Naming
| Convention | Example |
|---|---|
| File name | UserCard.vue PascalCase |
| Template usage | <UserCard /> or <user-card /> |
| Multi-word names | Required by ESLint to avoid HTML conflicts |
Practice: PostListItem
Create PostListItem.vue:
- Props:
id,title,published - Emit
selectwithidwhen row clicked - Emit
toggle-publishedwithidfrom a button
Parent maintains array of posts and handles events.
FAQ
mutate props in child?
Never—emit event or use v-model pattern.
props destructuring lose reactivity?
In script setup, defineProps destructuring is reactive in Vue 3.5+; when unsure, use props.x or toRefs(props).
emit vs callback prop?
Both work; emit is idiomatic for Vue components.
Required prop missing?
Vue warns in dev; TypeScript catches at build with typed props.
Boolean prop shorthand?
<Menu open /> equals :open="true" when prop name matches.
Pass functions as props?
Possible (@click handler) but prefer emit for child→parent communication.