Vue Component Communication & Props
Learn how Vue components communicate with each other through props, events, provide/inject, and other patterns:
Props - Parent to Child Communication
Props allow parent components to pass data down to child components. They are reactive and can be validated with types and validators.
<script setup>
// Basic props
const props = defineProps({
title: String,
count: Number,
user: Object,
tags: Array,
})
// Props with defaults and validation
const props = defineProps({
title: {
type: String,
required: true,
},
count: {
type: Number,
default: 0,
},
status: {
type: String,
default: 'pending',
validator: (value) => ['pending', 'active', 'completed'].includes(value),
},
user: {
type: Object,
required: true,
validator: (value) => value.id && value.name,
},
})
// TypeScript props
interface Props {
title: string
count?: number
user: { id: number; name: string }
}
const props = withDefaults(defineProps<Props>(), {
count: 0,
})
// Destructuring props (loses reactivity)
const { title, count } = props // ❌ Not reactive
// Use toRefs to maintain reactivity
import { toRefs } from 'vue'
const { title, count } = toRefs(props) // ✅ Reactive
</script>
<template>
<div>
<h1>{{ title }}</h1>
<p>Count: {{ count }}</p>
<p>User: {{ user.name }}</p>
</div>
</template>Events - Child to Parent Communication
Components emit custom events to communicate with their parent. defineEmits() is used to declare which events a component can emit.
<!-- Child Component -->
<script setup>
const emit = defineEmits(['update', 'delete', 'statusChange'])
// With TypeScript
const emit = defineEmits<{
update: [value: number]
delete: [id: string]
statusChange: [status: 'pending' | 'active' | 'completed']
}>()
const handleClick = () => {
emit('update', 42)
}
const handleDelete = (id) => {
emit('delete', id)
}
const handleStatusChange = () => {
emit('statusChange', 'completed')
}
</script>
<!-- Parent Component -->
<script setup>
import ChildComponent from './ChildComponent.vue'
const handleUpdate = (value) => {
console.log('Received:', value)
}
const handleDelete = (id) => {
console.log('Delete:', id)
}
</script>
<template>
<ChildComponent
@update="handleUpdate"
@delete="handleDelete"
@status-change="(status) => console.log('Status:', status)"
/>
</template>v-model - Two-Way Binding
v-model provides two-way data binding between parent and child components. It's syntactic sugar for passing a prop and listening to an event.
<!-- Custom Input Component -->
<script setup>
const props = defineProps(['modelValue'])
const emit = defineEmits(['update:modelValue'])
const updateValue = (event) => {
emit('update:modelValue', event.target.value)
}
</script>
<template>
<input
:value="modelValue"
@input="updateValue"
placeholder="Enter text"
/>
</template>
<!-- Parent Usage -->
<script setup>
import { ref } from 'vue'
import CustomInput from './CustomInput.vue'
const searchQuery = ref('')
</script>
<template>
<!-- v-model is shorthand for :modelValue and @update:modelValue -->
<CustomInput v-model="searchQuery" />
<p>You typed: {{ searchQuery }}</p>
</template>
<!-- Multiple v-models -->
<script setup>
// Child component
const props = defineProps(['title', 'content'])
const emit = defineEmits(['update:title', 'update:content'])
</script>
<template>
<div>
<input
:value="title"
@input="$emit('update:title', $event.target.value)"
/>
<textarea
:value="content"
@input="$emit('update:content', $event.target.value)"
/>
</div>
</template>
<!-- Parent usage -->
<script setup>
const title = ref('')
const content = ref('')
</script>
<template>
<MyComponent
v-model:title="title"
v-model:content="content"
/>
</template>provide() / inject() - Dependency Injection
provide() and inject() enable passing data from ancestor to descendant components without prop drilling.
<!-- Root/Ancestor Component -->
<script setup>
import { ref, provide, readonly } from 'vue'
const theme = ref('light')
const user = ref({ name: 'John', role: 'admin' })
// Provide reactive values
provide('theme', theme)
provide('user', readonly(user)) // Readonly to prevent modification
// Provide methods
const toggleTheme = () => {
theme.value = theme.value === 'light' ? 'dark' : 'light'
}
provide('toggleTheme', toggleTheme)
// Provide with symbol keys (recommended for libraries)
const ThemeSymbol = Symbol()
provide(ThemeSymbol, theme)
</script>
<!-- Descendant Component (any level deep) -->
<script setup>
import { inject } from 'vue'
// Inject with default value
const theme = inject('theme', 'light')
const user = inject('user')
const toggleTheme = inject('toggleTheme')
// Inject with symbol key
const ThemeSymbol = Symbol()
const themeFromSymbol = inject(ThemeSymbol)
// Type-safe injection with TypeScript
interface User {
name: string
role: string
}
const user = inject<User>('user')
const theme = inject<Ref<string>>('theme', ref('light'))
</script>
<template>
<div :class="theme">
<p>Current user: {{ user?.name }}</p>
<button @click="toggleTheme">Toggle Theme</button>
</div>
</template>Slots - Content Distribution
Slots allow parent components to pass template content to child components, enabling flexible component composition.
<!-- Card Component with slots -->
<template>
<div class="card">
<header v-if="$slots.header" class="card-header">
<slot name="header" />
</header>
<div class="card-body">
<!-- Default slot -->
<slot />
</div>
<footer v-if="$slots.footer" class="card-footer">
<slot name="footer" />
</footer>
</div>
</template>
<!-- Usage -->
<template>
<Card>
<template #header>
<h2>Card Title</h2>
<button>×</button>
</template>
<!-- Default slot content -->
<p>This is the main content of the card.</p>
<p>It can contain multiple elements.</p>
<template #footer>
<button>Cancel</button>
<button>Save</button>
</template>
</Card>
</template>
<!-- Scoped Slots - Pass data to parent -->
<script setup>
const items = ref([
{ id: 1, name: 'Item 1', price: 100 },
{ id: 2, name: 'Item 2', price: 200 },
])
</script>
<template>
<div>
<!-- Child exposes data through slot props -->
<div v-for="item in items" :key="item.id">
<slot :item="item" :index="index">
<!-- Fallback content -->
{{ item.name }}
</slot>
</div>
</div>
</template>
<!-- Parent receives and uses slot props -->
<template>
<ItemList>
<template #default="{ item, index }">
<div>
<span>{{ index + 1 }}.</span>
<strong>{{ item.name }}</strong>
<span>${{ item.price }}</span>
</div>
</template>
</ItemList>
</template>Keep your own version of these notes — editable, searchable, and organised by your stack.
Start free