~ 5 min read
Vue.js Patterns: Using Vue.js 3 Composition API for 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. Ref
s are mutable and allow you to set initial data such asconst 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 Ref
s?
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 Ref
s 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 thecontent
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 theprops.content
value to thecontent
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.