Passing Data with Props

+15 Mana ✨

Props are custom attributes you can register on a component. They let parent components pass data to child components.

Defining Props

Use defineProps() to declare what props a component accepts:

vue
<!-- src/components/GreetingCard.vue -->
<script setup lang="ts">
const props = defineProps<{
  name: string
  age?: number  // Optional prop
}>()
</script>

<template>
  <div class="greeting">
    <h2>Hello, {{ name }}!</h2>
    <p v-if="age">You are {{ age }} years old.</p>
  </div>
</template>

Passing Props

Pass props like HTML attributes:

vue
<!-- Parent component -->
<script setup lang="ts">
import GreetingCard from './components/GreetingCard.vue'
import { ref } from 'vue'

const userName = ref('Alice')
const userAge = ref(25)
</script>

<template>
  <!-- Static props -->
  <GreetingCard name="Bob" />
  <GreetingCard name="Charlie" :age="30" />
  
  <!-- Dynamic props with v-bind -->
  <GreetingCard :name="userName" :age="userAge" />
</template>

Prop Types

vue
<script setup lang="ts">
type Status = 'pending' | 'active' | 'completed'

type User = {
  id: number
  name: string
  email: string
}

const props = defineProps<{
  // Required string
  title: string
  
  // Optional number
  count?: number
  
  // Array of strings
  tags: string[]
  
  // Object type
  user: User
  
  // Union type
  status: Status
  
  // Boolean
  isActive: boolean
  
  // Function
  onUpdate: (value: string) => void
}>()
</script>

With Default Values

Use withDefaults() for default values:

vue
<script setup lang="ts">
type Props = {
  title: string
  count?: number
  items?: string[]
  theme?: 'light' | 'dark'
}

const props = withDefaults(defineProps<Props>(), {
  count: 0,
  items: () => [],  // Use factory for objects/arrays
  theme: 'light'
})
</script>

Using Props in the Template

Props are directly available in templates:

vue
<script setup lang="ts">
const props = defineProps<{
  title: string
  items: string[]
  showHeader: boolean
}>()
</script>

<template>
  <div class="container">
    <h1 v-if="showHeader">{{ title }}</h1>
    <ul>
      <li v-for="item in items" :key="item">{{ item }}</li>
    </ul>
  </div>
</template>

Props are Read-Only!

Never mutate props directly—they flow one-way from parent to child:

vue
<script setup lang="ts">
const props = defineProps<{
  count: number
}>()

// ❌ NEVER do this - will cause warnings and bugs
function increment() {
  props.count++  // Error: cannot mutate props
}

// ✅ Instead, emit an event to the parent
const emit = defineEmits<{
  update: [newCount: number]
}>()

function increment() {
  emit('update', props.count + 1)
}
</script>

Practical Example: Product Card

vue
<!-- src/components/ProductCard.vue -->
<script setup lang="ts">
type Product = {
  id: number
  name: string
  price: number
  image: string
  inStock: boolean
}

const props = withDefaults(defineProps<{
  product: Product
  showActions?: boolean
}>(), {
  showActions: true
})

const emit = defineEmits<{
  addToCart: [productId: number]
}>()

function formatPrice(price: number): string {
  return `$${price.toFixed(2)}`
}
</script>

<template>
  <div class="product-card">
    <img :src="product.image" :alt="product.name" />
    
    <div class="content">
      <h3>{{ product.name }}</h3>
      <p class="price">{{ formatPrice(product.price) }}</p>
      
      <span :class="['stock', { 'in-stock': product.inStock }]">
        {{ product.inStock ? 'In Stock' : 'Out of Stock' }}
      </span>
    </div>
    
    <div v-if="showActions" class="actions">
      <button 
        @click="emit('addToCart', product.id)"
        :disabled="!product.inStock"
      >
        Add to Cart
      </button>
    </div>
  </div>
</template>

<style scoped>
.product-card {
  border: 1px solid #e0e0e0;
  border-radius: 8px;
  overflow: hidden;
}

.product-card img {
  width: 100%;
  height: 200px;
  object-fit: cover;
}

.content {
  padding: 1rem;
}

.price {
  font-size: 1.25rem;
  font-weight: bold;
  color: #42b883;
}

.stock {
  font-size: 0.875rem;
  color: #e53e3e;
}

.stock.in-stock {
  color: #38a169;
}

.actions {
  padding: 1rem;
  border-top: 1px solid #e0e0e0;
}
</style>
vue
<!-- Using the ProductCard -->
<script setup lang="ts">
import ProductCard from './components/ProductCard.vue'
import { ref } from 'vue'

const products = ref([
  { id: 1, name: 'Laptop', price: 999.99, image: '/laptop.jpg', inStock: true },
  { id: 2, name: 'Phone', price: 699.99, image: '/phone.jpg', inStock: false },
])

function handleAddToCart(productId: number) {
  console.log('Adding product to cart:', productId)
}
</script>

<template>
  <div class="products">
    <ProductCard
      v-for="product in products"
      :key="product.id"
      :product="product"
      @add-to-cart="handleAddToCart"
    />
  </div>
</template>
✓ Completed