Component Communication Patterns in Vue.js
Props and Events: Parent-Child Communication
The fundamental approach for parent-child data flow involves passing data downward through properties and emitting events upward.
<!-- ProductDisplay.vue -->
<template>
<section>
<ItemCard :product="currentProduct" @add-to-cart="onAddItem" />
</section>
</template>
<script>
import ItemCard from './ItemCard.vue';
export default {
data() {
return {
currentProduct: {
id: 1,
name: 'Wireless Headphones',
price: 79.99
}
};
},
components: { ItemCard },
methods: {
onAddItem(productId) {
console.log('Item added:', productId);
}
}
};
</script>
<!-- ItemCard.vue -->
<template>
<article>
<h3>{{ product.name }}</h3>
<span class="price">${{ product.price }}</span>
<button @click="addToCart">Add to Cart</button>
</article>
</template>
<script>
export default {
props: {
product: {
type: Object,
required: true
}
},
methods: {
addToCart() {
this.$emit('add-to-cart', this.product.id);
}
}
};
</script>
Sibling Component Communication via Event Bus
Components without direct parent-child relationships can communicate through a shared event bus instance.
// eventHub.js
import Vue from 'vue';
export const eventHub = new Vue();
<!-- NotificationSender.vue -->
<template>
<div class="sender">
<input v-model="notificationText" placeholder="Type a notification" />
<button @click="broadcastNotification">Publish</button>
</div>
</template>
<script>
import { eventHub } from '../eventHub.js';
export default {
data() {
return {
notificationText: ''
};
},
methods: {
broadcastNotification() {
eventHub.$emit('notification', this.notificationText);
this.notificationText = '';
}
}
};
</script>
<!-- NotificationReceiver.vue -->
<template>
<div class="receiver">
<strong>Latest Notification:</strong>
<p v-if="latestNotification">{{ latestNotification }}</p>
<p v-else class="empty">No notifications yet</p>
</div>
</template>
<script>
import { eventHub } from '../eventHub.js';
export default {
data() {
return {
latestNotification: ''
};
},
mounted() {
eventHub.$on('notification', (text) => {
this.latestNotification = text;
});
},
beforeUnmount() {
eventHub.$off('notification');
}
};
</script>
Ancestor-Descendant Communication with provide/inject
For multi-level component trees, providing and injecting dependencies avoids prop drilling through intermediate components.
<!-- AppRoot.vue -->
<template>
<div id="app">
<NavigationBar />
<router-view />
</div>
</template>
<script>
import { provide } from 'vue';
import NavigationBar from './NavigationBar.vue';
export default {
components: { NavigationBar },
setup() {
const themeConfig = {
primaryColor: '#3498db',
darkMode: false,
fontSize: 16
};
provide('theme', themeConfig);
}
};
</script>
<!-- ThemeToggle.vue -->
<template>
<div class="theme-toggle">
<button @click="toggleDarkMode">
{{ currentTheme.darkMode ? 'Light Mode' : 'Dark Mode' }}
</button>
</div>
</template>
<script>
import { inject, computed } from 'vue';
export default {
setup() {
const theme = inject('theme');
const currentTheme = computed(() => theme);
const toggleDarkMode = () => {
theme.darkMode = !theme.darkMode;
};
return { currentTheme, toggleDarkMode };
}
};
</script>
Centralized State Management with Vuex
For complex applications with numerous interconnected components, Vuex provides a structured approach to managing shared applicatoin state.
// src/store/index.js
import { createStore } from 'vuex';
export default createStore({
state() {
return {
userSettings: {
username: '',
notifications: true,
language: 'en'
},
isAuthenticated: false
};
},
mutations: {
updateUsername(state, newName) {
state.userSettings.username = newName;
},
toggleNotifications(state) {
state.userSettings.notifications = !state.userSettings.notifications;
},
setAuthStatus(state, status) {
state.isAuthenticated = status;
}
},
actions: {
async login({ commit }, credentials) {
const response = await api.authenticate(credentials);
commit('setAuthStatus', true);
commit('updateUsername', response.username);
}
},
getters: {
currentUsername: (state) => state.userSettings.username,
hasUnreadNotifications: (state) => state.userSettings.notifications
}
});
<!-- UserProfile.vue -->
<template>
<div class="profile">
<input v-model="localUsername" @blur="saveUsername" />
<span>Welcome, {{ username }}</span>
</div>
</template>
<script>
import { ref } from 'vue';
import { useStore } from 'vuex';
export default {
setup() {
const store = useStore();
const localUsername = ref('');
const username = () => store.getters.currentUsername;
const saveUsername = () => {
store.commit('updateUsername', localUsername.value);
};
return { localUsername, username, saveUsername };
}
};
</script>
<!-- SettingsPanel.vue -->
<template>
<div class="settings">
<label>
<input type="checkbox" v-model="notificationsEnabled" />
Enable Notifications
</label>
</div>
</template>
<script>
import { ref, watch } from 'vue';
import { useStore } from 'vuex';
export default {
setup() {
const store = useStore();
const notificationsEnabled = ref(false);
watch(notificationsEnabled, (newValue) => {
store.commit('toggleNotifications');
});
return { notificationsEnabled };
}
};
</script>
Direct Component Access via $refs
When you need to invoke methods or access internal state of child components directly, refs provide a reference to the component instance.
<!-- MediaPlayer.vue -->
<template>
<div class="player">
<VideoElement ref="videoPlayer" />
<div class="controls">
<button @click="play">Play</button>
<button @click="pause">Pause</button>
<button @click="restart">Restart</button>
</div>
</div>
</template>
<script>
import VideoElement from './VideoElement.vue';
export default {
components: { VideoElement },
methods: {
play() {
this.$refs.videoPlayer.play();
},
pause() {
this.$refs.videoPlayer.pause();
},
restart() {
this.$refs.videoPlayer.reset();
}
}
};
</script>
<!-- VideoElement.vue -->
<template>
<div class="video-wrapper">
<video :src="videoSource" ref="videoTag"></video>
<p>Current Time: {{ currentTime }}s</p>
<p>Status: {{ playStatus }}</p>
</div>
</template>
<script>
export default {
data() {
return {
videoSource: '/assets/sample-video.mp4',
currentTime: 0
};
},
computed: {
playStatus() {
return this.$refs.videoTag?.paused ? 'Paused' : 'Playing';
}
},
methods: {
play() {
this.$refs.videoTag.play();
},
pause() {
this.$refs.videoTag.pause();
},
reset() {
this.$refs.videoTag.currentTime = 0;
this.$refs.videoTag.play();
}
}
};
</script>