Vue 3 Component Communication: 12+ Methods Explained

This comprehensive guide covers all major component communication patterns in Vue 3.2+ using Single File Components with <script setup> syntax.

Table of Contents

  1. Props
  2. Emits
  3. Expose / Ref
  4. Non-Props Attributes
  5. v-model
  6. Slots
  7. Provide / Inject
  8. Event Bus
  9. getCurrentInstance
  10. Vuex
  11. Pinia
  12. 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 this in 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 listener
  • emit(event, data): Trigger event
  • off(event, handler): Remove listener
  • all: 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.

Tags: vue vue3 javascript components frontend

Posted on Thu, 07 May 2026 01:10:19 +0000 by Phreak E