~ 5 min read

Vue.js Patterns: Using Vue.js 3 Composition API for Reactive Parent to Child Communication

share this story on
Vue.js revolves around a reactivity system, which is unlike React. In this article, we will explore how to use the Vue.js 3 Composition API to create a reactive parent to child communication.

Here’s the use-case: A parent Vue.js component needs to pass data to a child component. It does so using Props. The child component needs to receive this external data as its initial state, but also be able to mutate it internally, e.g: binding two-way data for a text input element.

The limitation we face while implementing that are as follows:

  • A prop is designed to be immutable. The child component shouldn’t mutate the data it receives from the parent component.
  • Refs are mutable and allow you to set initial data such as const content = ref(props.content), but depending how you use them, could end up as a pitfall.

What is the issue with using Vue.js’s Refs?

Refs are part of the Vue.js reactivity system. They are used to create reactive data and they are a proper building block of the Vue.js 3 Composition API.

However, the way you might be using them, per most of the guides I’ve seen, is not going to be helpful with defined use-case.

Consider the following code of a parent component:

<template>
  <child-component :content="content"></child-component>
</template>

In the child component, you might be looking at using refs by binding them to an initial value that originates from the parent component’s props like so:

<script setup>
import { ref } from 'vue';

defineProps({
  content: {
    type: String,
    default: ''
  }
});

const content = ref(props.content);
</script>

This is a valid code and will technically work but you’d have to be aware of Vue.js’s lifecycle hooks to understand why it’s not the best approach.

In this component setup convention, the ref() call would only be ever called once - when the component is mounted. This means that if the parent component changes the content prop, the child component will not be aware of that change.

How to use Refs properly? watch() to the rescue!

The solution to this problem is to use the watch() function, or its smarter sibling: watchEffect(). Like Refs, the watch functions are a part of the Vue.js reactivity system and allow you to watch for changes in a reactive object changes, regardless of the component’s lifecycle hooks.

Following is a complete code example.

We’ll show how to to pass default, initial values, from a parent component to a child component and set these as values using Vuetify 3 and Vue Composition API, while also enabling two-way binding.

The parent component is defined as follows, with a content prop that is passed to the child component and may be dynamically changed from the parent component based on its own state:

<template>
  <child-component :content="content"></child-component>
</template>

In the child component, we’ll define a text input element that will be bound to a content ref with Vue.js’s own v-model directive. This will allow us to mutate the value of the content ref from the child component, which achieves the two-way binding we’re looking for.

Also allowing the parent component to change the value of the content prop, which will be reflected in the child component.

<template>
    <v-text-field v-model="content" />
</template>

<script setup>
import { ref } from 'vue';

defineProps({
  content: {
    type: String,
    default: ''
  }
});

const content = ref(props.content);
</script>

To allow the child component to be aware of changes in the props passed to it we’ll use the watchEffect() function to watch for changes in the content prop. This will allow us to update the value of the content ref, which will be reflected in the child component.

Add the following code to the child component in the global scope next to the content variable definition:

<script setup>
import { watch } from 'vue';


watch(
  () => props.content,
  () => { content.value = props.content }
);
</script>

Let’s explain what is going on here with the watch() function.

The watch function you see here is part of Vue’s Composition API. Its purpose is to observe and react to changes on a specific reactive source. It could be a simple reactive reference (like a ref), or it could be a function that returns a reactive source.

Here’s how it works:

  • () => props.content: This is called the “source” function. It should return the value that you want to watch. In this case, it’s the content value passed from the props.

  • () => { content.value = props.content }: This is the callback function that gets executed when the source changes. In this case, it assigned the props.content value to the content ref.

You may also pass an additional 3rd argument options object such as { immediate: true } which means the callback should be run immediately after the watch gets created, even before the source has changed. This is useful for cases where you want the callback to run initially when setting up the watch.

We can instead use the watchEffect() function is a less verbose way to achieve the same result. The watchEffect() function will be called every time the content prop hydrates, and will update the value of the content ref, which will be reflected in the child component.

<script setup>
import { watchEffect } from 'vue';

 watchEffect(() => {
  content.value = props.content;
});
</script>

Bonus: using this pattern without the setup syntactic sugar

Here’s a generic re-write to the above, but without using the setup script convention, demonstrating how our use-case can be achieved using Vue 3’s Composition API:

import { ref, watchEffect } from 'vue';

export default {
  props: {
    initialData: {
      type: Array, // change this based on your data type
      default: () => ([]), // provide a default value
    },
  },
  setup(props) {
    // Make a local copy of the received prop
    const data = ref([...props.initialData]);

    // Update local data whenever the prop changes
    watchEffect(() => {
      data.value = [...props.initialData];
    });

    // Now you can manipulate `data` within your component

    return {
      data
    };
  }
};

In this example, initialData is a prop passed from the parent component. The data reactive variable is a local copy of initialData. The watchEffect function is used to update data whenever initialData changes, ensuring data always starts with the current value of initialData.

Since data is a local copy, it can be freely mutated inside the child component without affecting the prop’s value in the parent component.