This comprehensive guide covers all major component communication patterns in Vue 3.2+ using Single File Components with <script setup> syntax.
Table of Contents
- Props
- Emits
- Expose / Ref
- Non-Props Attributes
- v-model
- Slots
- Provide / Inject
- Event Bus
- getCurrentInstance
- Vuex
- Pinia
- mitt.js
1. Props
Parent-to-child data transfer using props.
Parent Component
<!-- Parent.vue -->
<template>
<ChildComponent :title="pageTitle" :item-count="10" />
</template>
<script setup>
import ChildComponent from './components/ChildComponent.vue'
const pageTitle = 'Welcome Page'
</script>
Child Component
<!-- ChildComponent.vue -->
<template>
<div>
<h1>{{ title }}</h1>
<p>Items: {{ itemCount }}</p>
</div>
</template>
<script setup>
const props = defineProps({
title: {
type: String,
required: true,
default: 'Default Title'
},
itemCount: {
type: Number,
default: 0
}
})
// Access in JS via props object
console.log(props.title)
</script>
Use defineProps() in <script setup> to declare props with full type inference.
2. Emits
Child-to-parent communication to trigger events and pass data upward.
Parent Component
<!-- Parent.vue -->
<template>
<div>Current Value: {{ userName }}</div>
<ChildComponent @update-user="handleUpdate" />
</template>
<script setup>
import { ref } from 'vue'
import ChildComponent from './components/ChildComponent.vue'
const userName = ref('Guest')
function handleUpdate(newName) {
userName.value = newName
}
</script>
Child Component
<!-- ChildComponent.vue -->
<template>
<button @click="sendUpdate">Update Parent</button>
</template>
<script setup>
const emit = defineEmits(['update-user'])
function sendUpdate() {
emit('update-user', 'John Doe')
}
</script>
Use defineEmits() to declare emitted events. Both defineProps and defineEmits are auto-imported in <script setup>.
3. Expose / Ref
Expose child component methods and properties for parent access via template refs.
Parent Component
<!-- Parent.vue -->
<template>
<div>Child Data: {{ childData }}</div>
<button @click="callChildMethod">Execute Child Method</button>
<ChildComponent ref="childRef" />
</template>
<script setup>
import { ref, onMounted } from 'vue'
import ChildComponent from './components/ChildComponent.vue'
const childRef = ref(null)
const childData = ref('')
onMounted(() => {
childData.value = childRef.value.getData()
})
function callChildMethod() {
childRef.value.setData('Updated from parent')
childData.value = childRef.value.getData()
}
</script>
Child Component
<!-- ChildComponent.vue -->
<template>
<div>{{ internalData }}</div>
</template>
<script setup>
import { ref } from 'vue'
const internalData = ref('Initial Value')
function getData() {
return internalData.value
}
function setData(value) {
internalData.value = value
}
// Expose methods to parent
defineExpose({
getData,
setData
})
</script>
By default, internal state is not exposed. Use defineExpose() to explicitly share properties and methods.
4. Non-Props Attributes
Access attributes not defined as props or emits via $attrs.
Parent Component
<!-- Parent.vue -->
<template>
<ChildComponent class="custom-class" style="color: blue" data-id="123" />
</template>
Single Root Child
<!-- ChildComponent.vue -->
<template>
<div>Attributes automatically applied to root element</div>
</template>
Multiple Roots - Use $attrs
<!-- ChildComponent.vue -->
<template>
<div :class="$attrs.class">Partial binding</div>
<div v-bind="$attrs">Full binding</div>
</template>
With multiple root elements, attributes don't auto-fallthrough. Use v-bind="$attrs" to manually apply.
5. v-model
Two-way binding with components.
Basic v-model
Parent
<template>
<ChildComponent v-model="searchQuery" />
</template>
<script setup>
import { ref } from 'vue'
const searchQuery = ref('')
</script>
Child
<template>
<input
:value="modelValue"
@input="$emit('update:modelValue', $event.target.value)"
/>
</template>
<script setup>
const props = defineProps(['modelValue'])
const emit = defineEmits(['update:modelValue'])
</script>
Multiple v-model Bindings
Parent
<template>
<ChildComponent
v-model:search="searchQuery"
v-model:filter="filterType"
/>
</template>
<script setup>
import { ref } from 'vue'
const searchQuery = ref('')
const filterType = ref('all')
</script>
Child
<template>
<input :value="search" @input="$emit('update:search', $event.target.value)" />
<select :value="filter" @change="$emit('update:filter', $event.target.value)">
<option value="all">All</option>
<option value="active">Active</option>
</select>
</template>
<script setup>
const props = defineProps(['search', 'filter'])
const emit = defineEmits(['update:search', 'update:filter'])
</script>
v-model Modifiers
Parent
<template>
<ChildComponent v-model.trim="inputText" />
</template>
<script setup>
import { ref } from 'vue'
const inputText = ref('')
</script>
Child
<template>
<div>{{ modelValue }}</div>
</template>
<script setup>
const props = defineProps({
modelValue: String,
modelModifiers: {
default: () => ({})
}
})
const emit = defineEmits(['update:modelValue'])
// Check for trim modifier and process value
if (props.modelModifiers.trim) {
emit('update:modelValue', props.modelValue.trim())
}
</script>
6. Slots
Pass HTML fragments to child components.
Default Slot
Parent
<template>
<CardComponent>
<h2>Card Title</h2>
<p>Card content goes here</p>
</CardComponent>
</template>
Child
<template>
<div class="card">
<slot></slot>
</div>
</template>
Named Slots
Parenet
<template>
<LayoutComponent>
<template v-slot:header>
<nav>Navigation content</nav>
</template>
<template v-slot:default>
<main>Main content</main>
</template>
<template v-slot:footer>
<footer>Footer content</footer>
</template>
</LayoutComponent>
</template>
Child
<template>
<header><slot name="header"></slot></header>
<main><slot></slot></main>
<footer><slot name="footer"></slot></footer>
</template>
Scoped Slots
Access child data with in slot content.
Parent
<template>
<ListComponent :items="userList">
<template v-slot:default="{ item, index }">
<li>{{ index }}: {{ item.name }} - {{ item.role }}</li>
</template>
</ListComponent>
</template>
<script setup>
import { ref } from 'vue'
const userList = ref([
{ name: 'Alice', role: 'Admin' },
{ name: 'Bob', role: 'User' }
])
</script>
Child
<template>
<ul>
<slot v-for="(item, index) in items" :item="item" :index="index"></slot>
</ul>
</template>
<script setup>
const props = defineProps({
items: {
type: Array,
default: () => []
}
})
</script>
7. Provide / Inject
Share data across deeply nested components without prop drilling.
Parent
<!-- Parent.vue -->
<template>
<IntermediateComponent />
</template>
<script setup>
import { ref, provide, readonly } from 'vue'
import IntermediateComponent from './IntermediateComponent.vue'
const theme = ref('dark')
const userData = ref({ name: 'Admin', level: 5 })
// Readonly prevents direct mutation from children
provide('theme', readonly(theme))
provide('user', userData)
provide('updateTheme', (newTheme) => {
theme.value = newTheme
})
</script>
Deep Child
<!-- DeepChild.vue -->
<template>
<div :class="theme">
<p>User: {{ user.name }}</p>
<button @click="changeTheme">Switch Theme</button>
</div>
</template>
<script setup>
import { inject } from 'vue'
const theme = inject('theme', 'light')
const user = inject('user')
const updateTheme = inject('updateTheme')
function changeTheme() {
updateTheme(theme.value === 'dark' ? 'light' : 'dark')
}
</script>
Use readonly() to create immutable references while allowing mutations through provided functions.
8. Event Bus
Custom event bus implementation for component communication.
Bus.js
import { ref } from 'vue'
export class EventBus {
constructor() {
this.listeners = {}
this.sharedData = ref('Shared message')
}
on(event, callback) {
if (!this.listeners[event]) {
this.listeners[event] = []
}
this.listeners[event].push(callback)
}
emit(event, payload) {
if (this.listeners[event]) {
this.listeners[event].forEach(cb => cb(payload))
}
}
off(event) {
if (this.listeners[event]) {
delete this.listeners[event]
}
}
}
export default new EventBus()
Parent Component
<!-- Parent.vue -->
<template>
<div>Message: {{ message }}</div>
<ChildComponent />
</template>
<script setup>
import { ref } from 'vue'
import Bus from './Bus.js'
import ChildComponent from './ChildComponent.vue'
const message = ref('')
Bus.on('messageChanged', (data) => {
message.value = data
})
</script>
Child Component
<!-- ChildComponent.vue -->
<template>
<button @click="emitChange">Trigger Event</button>
</template>
<script setup>
import Bus from './Bus.js'
function emitChange() {
Bus.emit('messageChanged', 'Hello from child')
}
</script>
9. getCurrentInstance
Access internal component instance (recommended for library authors only).
Note: This API is for advanced use cases. Avoid in application code. Not a replacement for
thisin Composition API.
Parent Component
<!-- Parent.vue -->
<template>
<div>Parent: {{ parentValue }}</div>
<button @click="accessChildren">Access Child Data</button>
<ChildComponent />
<ChildComponent />
</template>
<script setup>
import { ref, getCurrentInstance, onMounted } from 'vue'
import ChildComponent from './ChildComponent.vue'
const parentValue = ref('Parent Data')
onMounted(() => {
const instance = getCurrentInstance()
// Access child components via parent
})
</script>
Child Component
<!-- ChildComponent.vue -->
<template>
<div>
<input v-model="inputValue" />
<button @click="notifyParent">Get Parent Value</button>
</div>
</template>
<script>
export default { name: 'ChildComponent' }
</script>
<script setup>
import { ref, getCurrentInstance, onMounted, nextTick } from 'vue'
const inputValue = ref('')
onMounted(() => {
const instance = getCurrentInstance()
nextTick(() => {
// Access parent instance
const parent = instance.parent
const parentState = parent?.exposed?.parentValue || parent?.devtoolsRawSetupState?.parentValue
})
})
function notifyParent() {
const instance = getCurrentInstance()
// Modify parent data
}
</script>
10. Vuex
Centralized state management for Vue applications.
Installation
npm install vuex@next
Store Configuration
// store/index.js
import { createStore } from 'vuex'
export default createStore({
state: {
user: { name: '', isAuthenticated: false },
items: []
},
getters: {
isLoggedIn: (state) => state.user.isAuthenticated,
itemCount: (state) => state.items.length
},
mutations: {
SET_USER(state, user) {
state.user = user
},
ADD_ITEM(state, item) {
state.items.push(item)
}
},
actions: {
async fetchUser({ commit }) {
const user = await api.getUser()
commit('SET_USER', user)
},
addItem({ commit }, item) {
commit('ADD_ITEM', item)
}
},
modules: {
cart,
settings
}
})
Root Entry Point
// main.js
import { createApp } from 'vue'
import App from './App.vue'
import store from './store'
const app = createApp(App)
app.use(store)
app.mount('#app')
Component Usage
<script setup>
import { useStore } from 'vuex'
const store = useStore()
// Access state
console.log(store.state.user.name)
// Access getters
console.log(store.getters.isLoggedIn)
// Commit mutation
store.commit('ADD_ITEM', { id: 1, name: 'New Item' })
// Dispatch action
store.dispatch('addItem', { id: 2, name: 'Another Item' })
</script>
Key Vuex concepts:
- State: Single source of truth
- Getters: Computed properties
- Mutations: Synchronous state changes (required for tracking)
- Actions: Async operations
- Modules: Namespaced sub-stores
11. Pinia
Modern state management, likely to become Vuex 5.
Installation
npm install pinia
Store Setup
// store/index.js
import { createPinia } from 'pinia'
export default createPinia()
Store Definition
// store/user.js
import { defineStore } from 'pinia'
export const useUserStore = defineStore('user', {
state: () => ({
name: 'Guest',
role: 'user',
preferences: {}
}),
getters: {
isAdmin: (state) => state.role === 'admin',
displayName: (state) => `User: ${state.name}`
},
actions: {
setName(newName) {
this.name = newName
},
async fetchUser() {
const data = await api.fetchUser()
this.$patch(data)
}
}
})
Component Usage
<template>
<div>
<p>{{ userStore.displayName }}</p>
<button @click="updateName">Update</button>
</div>
</template>
<script setup>
import { storeToRefs } from 'pinia'
import { useUserStore } from '@/store/user'
const userStore = useUserStore()
const { name, isAdmin } = storeToRefs(userStore)
function updateName() {
userStore.setName('New Name')
}
</script>
Advantages over Vuex:
- Cleaner API
- Better TypeScript support
- No mutations (actions handle all updates)
- Built-in module support
- Simpler structure
12. mitt.js
Lightweight event emitter for cross-component communication.
Installation
npm i mitt
Basic Usage
// emitter.js
import mitt from 'mitt'
export default mitt()
Parent Component
<!-- Parent.vue -->
<template>
<ChildComponent />
</template>
<script setup>
import emitter from './emitter.js'
import ChildComponent from './ChildComponent.vue'
emitter.on('customEvent', (data) => {
console.log('Received:', data)
})
</script>
Child Component
<!-- ChildComponent.vue -->
<template>
<button @click="sendEvent">Send Event</button>
</template>
<script setup>
import emitter from '../emitter.js'
function sendEvent() {
emitter.emit('customEvent', { message: 'Hello' })
}
</script>
Methods Available:
on(event, handler): Register listeneremit(event, data): Trigger eventoff(event, handler): Remove listenerall: Clear all handlers
Summary
| Method | Direction | Use Case |
|---|---|---|
| Props | Parent → Child | Simple data passing |
| Emits | Child → Parent | Trigger parent actions |
| Expose/Ref | Parent controls child | Direct child manipulation |
| $attrs | Access fallthrough | Style/class inheritance |
| v-model | Bidirectional | Form inputs |
| Slots | Content projection | Reusable components |
| Provide/Inject | Deep nesting | Avoid prop drilling |
| Event Bus | Any components | Simple pub/sub |
| getCurrentInstance | Internal access | Advanced/library code |
| Vuex | Global state | Large applications |
| Pinia | Global state | Modern Vue 3 apps |
| mitt.js | Lightweight events | Performance-critical |
Choose the appropriate communication pattern based on your specific use case and application architecture.