Component communication in Vue3: sibling, parent, and child components

Constantin Druc · 28 Mar, 2022

A while back, I made a video on building a slide-over dialog component using HeadlessUI and VueJS. Inside that component, a button would open the slide-over.

However, there was no way to open the slide-over from the outside - making it tricky to place the button in different places - like in a top navigation bar.

It all boils down to: how can vue components communicate with each other?

It depends. But first, here’s a stripped-down version of the slide-over component:

1<!-- ShoppingCart.vue -->
2<script setup>
3import {ref} from 'vue';
4import {Dialog, DialogOverlay, DialogTitle, TransitionChild, TransitionRoot} from '@headlessui/vue';
5 
6let isOpened = ref(false);
7 
8function setIsOpened(value) {
9 isOpened.value = value;
10}
11</script>
12 
13<template>
14 <button @click="setIsOpened(true)">
15 Open cart
16 </button>
17 
18 <TransitionRoot :show="isOpened" appear as="template">
19 <Dialog :open="isOpened" @close="setIsOpened(false)">
20 <TransitionChild as="template">
21 <DialogOverlay/>
22 </TransitionChild>
23 
24 <TransitionChild>
25 <DialogTitle>Cart summary</DialogTitle>
26 <button @click="setIsOpened(false)">
27 Close
28 </button>
29 </TransitionChild>
30 </Dialog>
31 </TransitionRoot>
32</template>

Parent-Child component communication

Parent components communicate with their child components through props. Child components communicate back by emitting events.

If we want to open the slide-over from the outside, we need to move the controlling state into the parent component, pass it as a prop to the child component, and then set a listener for an event the child will emit to toggle the isOpened value.

1<!-- ParentComponent.vue -->
2<script setup>
3import ShoppingCart from "./ShoppingCart.vue";
4import {ref} from "vue";
5 
6const isOpened = ref(false);
7</script>
8 
9<template>
10 <button @click="isOpened = true">Open cart</button>
11 <ShoppingCart :is-opened="isOpened" @toggle="(value) => isOpened = value"/>
12</template>

The child component would have to accept the prop, use it to decide wether or not the slide-over should open, and emit a toggle event to update isOpened .

1<!-- ShoppingCart.vue -->
2<script setup>
3import {ref} from 'vue';
4import {Dialog, DialogOverlay, DialogTitle, TransitionChild, TransitionRoot} from '@headlessui/vue';
5 
6const props = defineProps({
7 isOpened: Boolean
8});
9 
10const emit = defineEmits(['toggle']);
11 
12function setIsOpened(value) {
13 emit('toggle', value);
14}
15</script>
16 
17<template>
18 <button @click="setIsOpened(true)">
19 Open cart
20 </button>
21 
22 <TransitionRoot :show="props.isOpened" appear as="template">
23 <Dialog :open="props.isOpened" @close="setIsOpened(false)">
24 <TransitionChild as="template">
25 <DialogOverlay/>
26 </TransitionChild>
27 
28 <TransitionChild>
29 <DialogTitle>Cart summary</DialogTitle>
30 <button @click="setIsOpened(false)">
31 Close
32 </button>
33 </TransitionChild>
34 </Dialog>
35 </TransitionRoot>
36</template>

Sibling components communication

If we were to move the “Open cart” button from the parent component into a different component, say a TopNavigation component, the button would stop working.

That’s because the relationship between those components changed. They are no longer parent-child components; they are sibling components, and we need a different pattern for that.

If before we moved the controlling state from the child to its parent, we now need to move it into a central piece of state to be shared between multiple components - this is also known as a store, and Vue3 makes it easy to do this.

We’ll create a new file, stores/cart.js that will simply export a reactive value:

1// stores/cart.js
2export const cart = ref({
3 isOpen: false,
4 setIsOpen(value) {
5 this.isOpen = value;
6 }
7});

All we have to do is import the cart store and replace the old isOpened references with cart.isOpened and replace setIsOpened() with cart.setIsOpened():

1<!-- TopNavigation.vue -->
2<script setup>
3import {cart} from "../stores/cart.js"
4</script>
5 
6<template>
7 <nav>
8 <button @click="cart.setIsOpened(true)">Open cart</button>
9 </nav>
10</template>
1<!-- ShoppingCart.vue -->
2<script setup>
3import {Dialog, DialogOverlay, DialogTitle, TransitionChild, TransitionRoot} from '@headlessui/vue';
4import {cart} from "../stores/cart.js";
5</script>
6 
7<template>
8 <button @click="cart.setIsOpened(true)">
9 Open cart
10 </button>
11 
12 <TransitionRoot :show="cart.isOpened" appear as="template">
13 <Dialog :open="cart.isOpened" @close="cart.setIsOpened(false)">
14 <TransitionChild as="template">
15 <DialogOverlay/>
16 </TransitionChild>
17 
18 <TransitionChild>
19 <DialogTitle>Cart summary</DialogTitle>
20 <button @click="cart.setIsOpened(false)">
21 Close
22 </button>
23 </TransitionChild>
24 </Dialog>
25 </TransitionRoot>
26</template>

Closing & caution

If you need to communicate between parent and child components, pass data through props and emit events from the child component.

If you need to communicate between sibling components, create a central piece of state that can be shared between the components that need it.

However, if you’re working on a large application, consider using Pinia - which is the state management library recommended by the Vue core team: https://pinia.vuejs.org/