Have you ever face this error -
Avoid mutating a prop directly since the value will be overwritten whenever the parent component re-renders. Instead, use a data or computed property based on the prop's value.
The reason why Vue doesn’t recommend to modify the prop
data because if the parent component has a different value, the data in the child component will be re-render.
How to solve it?
I’m gonna create a simple Vue component consist of input with v-model
attached to it and another Card component that accepts the v-model
data as a props
and displays it.
This is how the interface will looks alike.
App.vue
<template>
<div id="app" class="max-w-sm mx-auto py-16">
<div class="flex flex-col mb-6">
<div class="mb-2">
<label for="inline-full-name">Enter your number</label>
</div>
<div class="w-full">
<input
v-model="number"
id="inline-full-name"
class="input"
type="number"
/>
</div>
</div>
<Card :number="number" />
</div>
</template>
<script>
import Card from "@/components/Card";
export default {
name: "app",
components: {
Card,
},
data() {
return {
number: 20,
};
},
};
</script>
<style lang="postcss">
#app {
font-family: "Avenir", Helvetica, Arial, sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
text-align: center;
color: #2c3e50;
margin-top: 60px;
}
label {
@apply blocktext-gray-500font-boldmb-1pr-4text-left;
}
.input {
@apply bg-gray-200appearance-noneborder-2border-gray-200roundedw-fullpy-2px-4text-gray-700leading-tight;
}
.input:focus {
@apply outline-nonebg-whiteborder-purple-500;
}
</style>
Card.vue
<template>
<div
class="bg-red-100 flex justify-between items-center border border-red-400 text-red-700 px-4 py-3 rounded relative"
role="alert"
>
<strong class="font-bold">Your number is {{ number }}</strong>
<svg
class="fill-current h-6 w-6 text-red-500"
@click.prevent="addNumber"
xmlns="http://www.w3.org/2000/svg"
viewBox="002424"
width="24"
height="24"
>
<path
class="heroicon-ui"
d="M1711a1100102h-4v4a11001-20v-4H7a110010-2h4V7a1100120v4h4z"
/>
</svg>
</div>
</template>
<script>
export default {
props: {
number: {
required: true,
},
},
methods: {
addNumber() {
this.number++;
},
},
};
</script>
This is how the interface works.
Every-time user type in the input, it will reflect directly in the card below. It’s because we're passing v-model: number
as prop
in the Card
component.
Any value that user’s typing will reflect immediately.
My next question is what’s going to happen if we’re going to manipulate the data in the child component, which is the Card
component.
I’ll add one button in the Card
component that triggers a method
that increase the value
.
Card.vue
<script>
export default {
props: {
number: {
required: true,
},
},
methods: {
addNumber() {
this.number++;
},
},
};
</script>
Surprisingly, this is what’s happening.
When we want to increase the value in the child component, Vue gives us an error message that this is not recommended way.
Why is it not recommend?
If the data v-model in the parent is changed, the data in the child component will re-render.
For example, I add any number in the input, and I increase the number
using the plus button. What happens if I add a new value in the input
?
The value that we increased just now is getting a new value from the input.
Get it?
How do we solve it?
Since we cannot modify the props data directly, so we need to replicate the props
data into a variable.
In the addNumber
methods, assign the props into a new variable, and do the calculation.
let myNumber = this.number;
myNumber++;
The next question is, how do we send the new number value to the parent? 🤔
Thank god, there is a custom event to solve it. We need to add the custom event in the addNumber
methods.
this.$emit("update-number", myNumber);
It’s mean that we’re sending a myNumber
data through update-number
event name. Since we’re sending a custom data, the parent component needs to listen to the update-number
event.
In the App.vue
, add the listener to the Card
component.
<Card:number="number" @update-number="update"/>
If there’s an emit
event called update-number
triggered in the child component, update
function will be called.
In the App.vue
file, add the update
function in the methods
section.
methods:{
update(number){
this.number=number;
}
}
Remember, when we’re sending the custom event, we’re sending myNumber
as a parameter. So, in the update
function need to have one parameter to get the data.
this.$emit("update-number", myNumber);
After that, the new value we get from the custom event will be assigned to the number
data. Since, the child component data will be re-render if every time the props
data changed, we can see the instant update in the Card
component.
Source Code
App.vue
<template>
<div id="app" class="max-w-sm mx-auto py-16">
<div class="flex flex-col mb-6">
<div class="mb-2">
<label for="inline-full-name">Enter your number</label>
</div>
<div class="w-full">
<input
v-model="number"
id="inline-full-name"
class="input"
type="number"
/>
</div>
</div>
<Card :number="number" @update-number="update" />
</div>
</template>
<script>
import Card from "@/components/Card";
export default {
name: "app",
components: {
Card,
},
data() {
return {
number: 20,
};
},
methods: {
update(number) {
this.number = number;
},
},
};
</script>
<style lang="postcss">
#app {
font-family: "Avenir", Helvetica, Arial, sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
text-align: center;
color: #2c3e50;
margin-top: 60px;
}
label {
@apply block text-gray-500 font-bold mb-1 pr-4 text-left;
}
.input {
@apply bg-gray-200 appearance-none border-2 border-gray-200 rounded w-full py-2 px-4 text-gray-700 leading-tight;
}
.input:focus {
@apply outline-none bg-white border-purple-500;
}
</style>
Card.vue
<template>
<div
class="bg-red-100 flex justify-between items-center border border-red-400 text-red-700 px-4 py-3 rounded relative"
role="alert"
>
<strong class="font-bold">Your number is {{ number }}</strong>
<svg
class="fill-current h-6 w-6 text-red-500 cursor-pointer"
@click.prevent="addNumber"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
width="24"
height="24"
>
<path
class="heroicon-ui"
d="M17 11a1 1 0 0 1 0 2h-4v4a1 1 0 0 1-2 0v-4H7a1 1 0 0 1 0-2h4V7a1 1 0 0 1 2 0v4h4z"
/>
</svg>
</div>
</template>
<script>
export default {
props: {
number: {
required: true,
},
},
methods: {
addNumber() {
let myNumber = this.number;
myNumber++;
this.$emit("update-number", myNumber);
},
},
};
</script>