Vue.js Props, $emit And Passing Reference Types As Props
August 18, 2020
In any component based framework, we will soon have the need to pass data between components.
Vue.js is no different and in both version 2 and 3, we do this with props and $emit.
TL;DR
Pass data from a parent to child with props- do not modify original props, take a copy and send back to parent with a custom event.
If passing a Javascript reference type such as an object as a prop, you can modify from the child component, since the prop is a reference to the original object/array. However you should still consider taking a copy and emitting a custom event.
Props
Let's start with props. Props are a way to pass data from a parent component to a child component.
Here is a basic example of our App.vue (wrapper) containing a child component:
// App.vue
<template>
<div id="app">
<img width="25%" src="./assets/logo.png">
<ChildComponent message="Hello Vue!"/>
</div>
</template>
<script>
import ChildComponent from "./components/ChildComponent";
export default {
name: "App",
components: {
ChildComponent
}
};
</script>
Our child component has a "message" attribute with some text. This attribute is a "prop" and it means inside of our child component, we now have access to this message value:
// ChildComponent.vue
<template>
<div>
<h1>{{ message }}</h1>
</div>
</template>
<script>
export default {
name: "Child",
props: {
message: String
}
};
</script>
This is directly outputting our message prop to the browser, if we wanted to mutate this prop directly, it would throw an error. This is because props are only intended to be used to pass data down to a child. If they could also pass data back to the parent, things could quickly become hard to maintain, especially if we have multiple components which rely on the same prop.
But, coding is not always this simple, we often want to pass a value back up to the parent. Maybe even the new prop value.
For this case we can take a copy of the prop, store in data, and update it in any way we want. For this example I am using a text input, and 2 way data binding with v-model:
// ChildComponent.vue
<template>
<div>
<input type="text" v-model="localMessage">
<p>{{ localMessage }}</p>
</div>
</template>
<script>
export default {
name: "Child",
props: {
message: String
},
data() {
return {
// take a copy of the props, and sync with the input field
localMessage: this.message
};
}
};
</script>
Our original prop is now copied to localMessage, and we now need a way to pass this localMessage back to the parent.
$emit
Which brings us on to $emit. While props are used to pass data from a parent to child, we "emit" custom events to pass data from the child to parent. This makes our components more predictable so we know what data a component is sending and receiving.
The $ prefix is the way Vue declares we are using a global property which is available in all instances/components.
In Vue we listen for events using v-on, or the @ shorthand, i.e @click etc. In this example, we are creating a custom event called changeMessage, which we will listen to be triggered from the child:
// App.vue
<template>
<div id="app">
<img width="25%" src="./assets/logo.png">
<ChildComponent
message="Hello Vue in CodeSandbox!"
// listen for our custom event (changeMessage) to be triggered from child
// then set a new data property called newMessageFromChild with the passed event data
@changeMessage="newMessageFromChild = $event"
/>
// show new message in the template
<p>from child: {{newMessageFromChild}}</p>
</div>
</template>
<script>
import ChildComponent from "./components/ChildComponent";
export default {
name: "App",
components: {
ChildComponent
},
data() {
return {
newMessageFromChild: ""
};
}
};
</script>
Once received, we store the data into a data property called newMessageFromChild, and display it in the template.
Over in the child component, we can then call our custom event, passing the localMessage as the data:
<template>
<div>
<!-- call a method when a key is lifted -->
<input type="text" v-model="localMessage" @keyup="update">
<p>{{ localMessage }}</p>
</div>
</template>
<script>
export default {
name: "Child",
props: {
message: String
},
data() {
return {
localMessage: this.message
};
},
// which will trigger our custom event:
methods: {
update() {
this.$emit("changeMessage", this.localMessage);
}
}
};
</script>
And this is it, we can pass props down, end pass data up with custom events. You can find a full example here:
https://codesandbox.io/s/vuejs-parent-child-communication-qnn3n
Passing reference types as props
However, there is something to watch our for though, and this is a quote from the official Vue.js documentation:
" Note that objects and arrays in JavaScript are passed by reference, so if the prop is an array or object, mutating the object or array itself inside the child component will affect parent state."
What does this mean?
Well for primitive values such as Strings, Numbers etc, we just need to do as we looked at earlier. We pass a string for example, take a copy and send back data if required.
As the quote states, when we pass an object or array, we are passing a reference to it, rather than the actual value. Meaning if we pass an object as a prop, then modify that prop in the child, it will update the parent data, since we are referencing the same thing. This direct modification means we do not need to use $emit to update the parents state.
This sounds a bit strange considering we have already spoke about not modifying props directly, but this is the way Javascript works. And it all works with Vue.js too.
However, in practice, choosing to do this is is personal preference. You may still want to consider taking a copy and emitting a custom event. Especially on larger apps where this would make it much easier to track changes, debug and also makes our components less tightly coupled.