Step 8 of 30
defineProps, defineEmits, slots, provide/inject, one-way data flow, component composition
props เข้า, events ออก, slots สำหรับ layout — กติกาที่ทำ component tree จัดการได้เมื่อแอปโต
Components are the units of a Vue UI. Data flows down through props; intent flows up through emitted events; slots distribute content into a component's layout.
Every non-trivial interface is a component tree. Getting the data flow right — props in, events out, never mutating what you do not own — is what keeps a growing frontend debuggable. These rules are identical in spirit to React props/callbacks and to how your Elysia handlers accept typed requests and return typed responses: contracts at every boundary.
<!-- TaskCard.vue -->
<script setup lang="ts">
interface Task {
id: string
title: string
completed: boolean
}
const props = defineProps<{ task: Task }>()
</script>
<template>
<div class="card" :class="{ done: task.completed }">
{{ task.title }}
<button v-if="!task.completed" @click="emit('complete', task.id)">
Done
</button>
</div>
</template>
defineProps is a compiler macro — no import needed. The prop type is the contract; passing a Task | null fails type-check in dev.
const emit = defineEmits<{
complete: [taskId: string]
remove: [taskId: string]
}>()
The parent handles the event and owns the state change:
<TaskCard
:task="task"
@complete="tasks.complete($event)"
@remove="tasks.remove($event)"
/>
One-way flow: the child signals, the parent decides.
<!-- Card.vue -->
<template>
<div class="border rounded-lg p-4">
<slot name="header" />
<slot /> <!-- default slot -->
</div>
</template>
<!-- usage -->
<Card>
<template #header><h3>Today</h3></template>
<p>3 tasks remaining</p>
</Card>
Slots compose layout; props compose data.
provide / injectFor deeply nested values, prop-drilling through five layers is worse than one explicit provider:
// ancestor
provide('session', readonly(session)) // readonly prevents mutation by children
// descendant
const session = inject<Session>('session')
When provide/inject spreads across many components, promote it to a Pinia store.
Loading diagram...
Small components with narrow props beat god-components with twenty of them.
props.task.completed = true in the child breaks ownership. Emit and let the parent update state.defineProps<{...}> gives free compile-time safety — losing it costs hours.