Avoid Mutating A Prop Directly

September 12, 2019 (5y ago)

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.

_error

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.

_interface

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.

_part-1

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 methodthat increase the value.

Card.vue

<script>
export default {
  props: {
    number: {
      required: true,
    },
  },
  methods: {
    addNumber() {
      this.number++;
    },
  },
};
</script>

Surprisingly, this is what’s happening.

_part-2

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?

_part-3

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.

_part-4

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>