VuVue · Lesson 4 of 7

Components & Props

Vue components communicate via props (parent → child) and emits (child → parent). This one-directional data flow makes applications predictable — you always know where data comes from.

HTML
<!-- UserCard.vue — child component -->
<template>
  <div class="user-card">
    <img :src="avatar" :alt="name" />
    <h3>{{ name }}</h3>
    <p>{{ role }}</p>
    <button @click="$emit('follow', userId)">Follow</button>
  </div>
</template>

<script setup>
// defineProps — declare incoming props
const props = defineProps({
  userId:  { type: Number, required: true },
  name:    { type: String, required: true },
  avatar:  { type: String, default: '/default-avatar.png' },
  role:    { type: String, default: 'user' },
})

// defineEmits — declare events this component can emit
const emit = defineEmits(['follow', 'unfollow'])

// Or with TypeScript:
// const props = defineProps<{ userId: number; name: string; role?: string }>()
// const emit  = defineEmits<{ follow: [userId: number] }>()
</script>

<!-- App.vue — parent using UserCard -->
<template>
  <div>
    <UserCard
      v-for="user in users"
      :key="user.id"
      :userId="user.id"
      :name="user.name"
      :role="user.role"
      @follow="handleFollow"
    />
  </div>
</template>

<script setup>
import UserCard from './UserCard.vue'
import { ref } from 'vue'

const users = ref([
  { id: 1, name: "Alice", role: "admin" },
  { id: 2, name: "Bob",   role: "user" },
])

function handleFollow(userId) {
  console.log(`Following user ${userId}`)
}
</script>
HTML
<!-- Slots — pass template content into a component -->

<!-- Card.vue -->
<template>
  <div class="card">
    <header v-if="$slots.header" class="card-header">
      <slot name="header" />
    </header>
    <div class="card-body">
      <slot />  <!-- default slot -->
    </div>
    <footer v-if="$slots.footer" class="card-footer">
      <slot name="footer" />
    </footer>
  </div>
</template>

<!-- Using Card with named slots -->
<Card>
  <template #header>
    <h2>Card Title</h2>
  </template>

  <p>This goes in the default slot.</p>
  <p>Multiple elements are fine.</p>

  <template #footer>
    <button>Save</button>
    <button>Cancel</button>
  </template>
</Card>

<!-- Scoped slots — component passes data back to the parent template -->
<!-- DataTable.vue exposes each row's data via scoped slot -->
<DataTable :rows="users">
  <template #row="{ row, index }">
    <td>{{ index + 1 }}</td>
    <td>{{ row.name }}</td>
    <td>{{ row.email }}</td>
  </template>
</DataTable>
✦ Tip
Keep props simple — pass data down, emit events up. If you find yourself passing props through 3+ levels, that's a sign to use `provide`/`inject` (Vue's context API) or a state management library like Pinia.