Vue Components Communication

By

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.

Tagged: Vue.js · All topics

Want me to talk about your product? You can sponsor this site.

~~~

Related posts about vue: