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:

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

vue
<script setup lang="ts">
withDefaults(
  defineProps<{
    title: string;
    size?: "sm" | "md" | "lg";
  }>(),
  {
    size: "md",
  }
);
</script>

Runtime declaration alternative (JavaScript):

vue
<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:

vue
<!-- Child -->
<script setup lang="ts">
defineProps<{ postCount: number }>();
</script>
 
<!-- Parent -->
<ProfileCard :post-count="42" />

One-Way Data Flow

Do not assign to props:

vue
<!-- 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):

vue
<SaveButton @save="handleSave" @cancel="handleCancel" />

Emit with Payload

vue
<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:

vue
<TodoRow @update="onUpdate" @remove="onRemove" />
typescript
function onUpdate(todo: Todo) {
  // merge into list
}
 
function onRemove(id: number) {
  // filter list
}

validate Emits (Optional)

Runtime names array:

vue
<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 defineModelForms and v-model.

Global vs Local Registration

This track uses local imports:

typescript
import UserCard from "./components/UserCard.vue";

Avoid app.component() global registration except plugins—harder to trace dependencies.

Component Naming

ConventionExample
File nameUserCard.vue PascalCase
Template usage<UserCard /> or <user-card />
Multi-word namesRequired by ESLint to avoid HTML conflicts

Practice: PostListItem

Create PostListItem.vue:

  • Props: id, title, published
  • Emit select with id when row clicked
  • Emit toggle-published with id from 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.