Vue Components Communication
By Flavio Copes
Learn how Vue 3 components communicate: props down, emits up, provide/inject for distant ancestors, and Pinia when you need shared app state.
Components in Vue can communicate in various ways.
Props
The first way is using props.
Parents “pass down” data by adding attributes to the component tag:
<template>
<div>
<Car color="green" />
</div>
</template>
<script setup>
import Car from './components/Car.vue'
</script>
Props are one-way, from parent to child. When the parent changes the prop, the child updates, but the child should never mutate a prop it received.
Using Events to communicate from children to parent
Children send messages up by emitting events.
With the Options API you call this.$emit():
<script>
export default {
name: 'Car',
methods: {
handleClick() {
this.$emit('clickedSomething')
}
}
}
</script>
With <script setup>, declare emits explicitly:
<script setup>
const emit = defineEmits(['clickedSomething'])
function handleClick() {
emit('clickedSomething')
}
</script>
The parent listens with v-on / @:
<template>
<div>
<Car @clickedSomething="handleClickInParent" />
</div>
</template>
<script setup>
function handleClickInParent() {
// ...
}
</script>
Pass payloads as extra arguments:
emit('clickedSomething', param1, param2)
function handleClickInParent(param1, param2) {
// ...
}
This is the props down, events up pattern, and it covers most parent/child communication.
provide / inject
When the data has to travel several levels down the tree, passing it as a prop at every step gets tedious.
provide / inject lets an ancestor expose a value to any descendant, however deep:
<!-- ancestor -->
<script setup>
import { provide, ref } from 'vue'
const theme = ref('dark')
provide('theme', theme)
</script>
<!-- deep child -->
<script setup>
import { inject } from 'vue'
const theme = inject('theme')
</script>
This is a good fit for things like the current theme or the logged-in user. When many unrelated screens need to read and change the same state, a store works better.
Shared state with Pinia
In Vue 2 the common answer for components that were not parent and child was a global Event Bus: you called this.$root.$emit() in one component and this.$root.$on() in another, or you created an empty new Vue() instance just to pass events around.
That does not work in Vue 3, because $on, $off and $once were removed from the component instance. If you really want a bus, the Vue docs point to a tiny library like mitt, but for shared state the better tool is Pinia, the official store for Vue 3 (my Vuex post covers the store pattern and when to pick Pinia over Vuex).
A store is a defineStore() call with a name and the state, getters and actions you want to share:
import { defineStore } from 'pinia'
export const useCartStore = defineStore('cart', {
state: () => ({
items: []
}),
actions: {
add(item) {
this.items.push(item)
}
}
})
Any component can then call useCartStore() and read or change the same data. You install Pinia once on the app with app.use(createPinia()).
Props and emits remain the right tool for a parent and its direct child. provide / inject and Pinia are for everything else.
Want me to talk about your product? You can sponsor this site.