Error unexpected mutation of prop vue no mutating props

Mutating props is an anti-pattern. You should use computed properties instead.

If you are using Vue 3 + ESLint and try to mutate a prop in your Vue component, you should see the following error:

Unexpected mutation of «todo» prop. eslintvue/no-mutating-props

If you are using Vue 2, Vue will throw the following error/warning in your browser’s console:

Message in browser console

[Vue warn]: 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.

Explanation

First, we look at the official documentation to understand why Vue throws that error.

Why do we see the error

When objects and arrays are passed as props, while the child component cannot mutate the prop binding, it will be able to mutate the object or array’s nested properties. This is because JavaScript objects and arrays are passed by reference, and it is unreasonably expensive for Vue to prevent such mutations.

Why is it a problem

The main drawback of such mutations is that it allows the child component to affect the parent state in a way that isn’t obvious to the parent component, potentially making it more difficult to reason about the data flow in the future. As a best practice, you should avoid such mutations unless the parent and child are tightly coupled by design.

Recommended solution

In most cases, the child should emit an event to let the parent perform the mutation.

Demo

Let’s look at a code example. I’m using a Todo app to demonstrate the error. The source code is interactively available on StackBlitz:

Let’s start by taking a look at the TodoList.vue component:

<script setup lang="ts">import { ref, Ref } from 'vue'import TodoItem from './TodoItem.vue'const todos: Ref<Array<{ id: number; name: string; completed: boolean }>> = ref([  { id: 1, name: 'Buy milk', completed: false },  { id: 2, name: 'Clean house', completed: false },])</script><template>  <h2>Todo</h2>  {{ todos }}  <div v-if="todos.length > 0" class="todo-list">    <TodoItem v-for="todoItem of todos" :key="todoItem.id" :todo="todoItem" />  </div>  <span v-else>Nothing todo</span></template><style scoped>.todo-list {  display: flex;  flex-direction: column;}</style>

TodoList.vue contains a reactive variable todos that includes an array of Todo items. In the template, each item is rendered via TodoItem.vue component:

<script setup lang="ts">defineProps<{  todo: { id: number; name: string; completed: boolean }}>()</script><template>  <div class="container">    <p>{{ todo.name }}</p>    Completed?    <input v-model="todo.completed" type="checkbox" />  </div></template><style scoped>.container {  border: 1px solid white;  border-radius: 10px;  padding: 10px;}</style>

Assuming ESLint is correctly configured, you should see the following ESLint error if you open TodoItem.vue in your editor:

ESLint error in VS Code

Mutating props is an anti-pattern

We want to write components that are easy to maintain.

In a maintainable component, only the component itself should be able to change its own state.

Additionally, only the component’s parent should be able to change the props.

These two rules are essential to ensure Vue’s One-Way Data Flow to make our app’s data flow easier to understand.

If we mutate the props, we violate both rules and break Vue’s data flow!

In addition, every time the parent component is updated, all props in the child component will be refreshed with the latest value.

Solution

In our example, instead of mutating the prop, we emit an event to the parent. The parent is then responsible for updating the Todo list correctly.

Let’s use a writeable computed property in TodoItem.vue. Its getter access the p‰rop’s value, and the setter emits an event to the parent:

<script setup lang="ts">import { computed } from 'vue'const props = defineProps<{  todo: { id: number; name: string; completed: boolean }}>()const emit = defineEmits<{  (e: 'update-completed', value: boolean): void}>()const completedInputModel = computed({  // getter  get() {    return props.todo.completed  },  // setter  set(newValue: boolean) {    emit('update-completed', newValue)  },})</script><template>  <div class="container">    <p>{{ todo.name }}</p>    Completed?    <input v-model="completedInputModel" type="checkbox" />  </div></template><style scoped>.container {  border: 1px solid white;  border-radius: 10px;  padding: 10px;}</style>

Finally, we need to react to the emitted event in TodoList.vue and update the todos accordingly:

<script setup lang="ts">import { ref, Ref } from 'vue'import { Todo } from './TodoItem.model'import TodoItem from './TodoItem.vue'const todos: Ref<Array<Todo>> = ref([  { id: 1, name: 'Buy milk', completed: false },  { id: 2, name: 'Clean house', completed: false },])const onUpdateCompleted = (updatedTodo: Todo) => {  todos.value = todos.value.map((todo) => {    if (todo.id === updatedTodo.id) {      todo = updatedTodo    }    return todo  })}</script><template>  <h2>Todo</h2>  {{ todos }}  <div v-if="todos.length > 0" class="todo-list">    <TodoItem v-for="todoItem of todos" :key="todoItem.id" :todo="todoItem" @update-completed="onUpdateCompleted" />  </div>  <span v-else>Nothing todo</span></template><style scoped>.todo-list {  display: flex;  flex-direction: column;}</style>

The prop is not mutated with this solution, and we correctly use the one-way data flow in our Vue application.

If you liked this Vue tip, follow me on Twitter to get notified about new tips, blog posts, and more. Alternatively (or additionally), you can subscribe to my weekly Vue newsletter.

nop33.eth

During last summer I was working on a Vue (and Vuex) side-project. I hadn’t touched it for almost 2 months due to lack of time and now I finally found the time to work on it again. Naturally, the first thing I do in these cases is to check that all my packages are up to date. Of course, shit loads of things changed these past 2 months in Frontendland :P

yarn upgrade

After upgrading all my packages I noticed that eslint is complaining about something. Apparently, I was violating the vue/no-mutating-props rule (since I am using the plugin:vue/essential eslint plugin) because I am changing the value of a prop passed from a parent component to a child component, within the child component. The official docs declare this as an anti-pattern with the following explanation:

Due to the new rendering mechanism, whenever the parent component re-renders, the child component’s local changes will be overwritten.

Seems like I haven’t thought my component design very thoroughly… Oopsie! Time to fix it.

To give a bit of context, the screen where the error appeared included 2 components:

  • /src/views/ItemAdd.vue (red box)
  • /src/components/ItemForm.vue (green box)

App screenshot

and the (simplified) code looked like this:

<!-- /src/views/ItemAdd.vue -->
<template>
  <div>
    <v-toolbar>
      <!-- ... -->
    </v-toolbar>
    <v-main>
      <ItemForm :item="item" />
    </v-main>
  </div>
</template>

<script>
  import ItemForm from "@/components/ItemForm.vue"

  export default {
    components: {
      ItemForm,
    },
    props: ["flatId"],
    data: () => {
      return {
        flat: {},
        item: {
          name: "",
          // ...
        },
      }
    },
    created() {
      this.flat = this.getFlat(this.flatId)
      this.item.name = this.flat.name
      // ...
    },
    methods: {
      // ...
    },
  }
</script>

<!-- /src/components/ItemForm.vue -->
<template>
  <v-form>
    <v-text-field v-model="item.name" label="Item name" />
    <!-- ... -->
  </v-form>
</template>

<script>
  export default {
    props: ["item"],
  }
</script>

Enter fullscreen mode

Exit fullscreen mode

This was working perfectly fine :P The v-model was modifying the property passed to the child component and I didn’t have to pass it back to the parent component so that it can be saved when clicking the Save button of the parent component’s toolbar. But that’s a bad pattern.

How NOT to do it

Since I’ve been using Vuex, my initial thought to tackle this problem was to use (what else) the central store.

I started by updating the store, adding the new item property and creating the necessary mutations and actions:

// /src/store/index.js
export default new Vuex.Store({
  state: {
    item: {
      name: '',
      // ...
    },
    // ...
  },
  mutations: {
    SET_ITEM_NAME (state, name) {
      state.name = name
    },
    // ...
  },
  actions: {
    setItemName ({ commit }, name) {
      commit('SET_ITEM_NAME', name)
    },
    // ...
  }
}

Enter fullscreen mode

Exit fullscreen mode

continued by updating the FormAdd.vue view component, removing the unnecessary prop and updating the value in the store:

- <ItemForm :item="item" />
+ <ItemForm />

...

   created () {
     this.flat = this.getFlat(this.flatId)
-    this.item.name = this.flat.name
+    this.$store.dispatch('setItemName', this.flat.name)
   },

Enter fullscreen mode

Exit fullscreen mode

Finally, updated the ItemForm.vue component to remove the prop and update the v-model to use the value from the store instead:

- <v-text-field v-model="item.name" label="Item name" />
+ <v-text-field v-model="name" label="Item name" />

...

 export default {
   props: [
-    'item',
     'allFlatmates'
   ],
+  computed: {
+    name: {
+      get () {
+        return this.$store.state.item.name
+      },
+      set (value) {
+        this.$store.dispatch('setItemName', value)
+      }
+    },
+ }

Enter fullscreen mode

Exit fullscreen mode

This solution seems to work initially. I created an item and it worked. Only to realize that the next time I tried to create an item, the form was already initialized with the data I inserted in the previous item. Dah! The store needs to be reset after every item creation. Which means I needed to:

  • generate the default state of the item and return it through a function
  • iterate through every property of the item property stored in the store and deep copy the default values
  • create more actions and mutations
  • and I haven’t even gone into the FormEdit.vue view component yet.

Meh…

Using the central store doesn’t look like the right way for this use-case.

How to do it with v-model

Taking a step back and considering what options there are for parent-child component communication in Vue, the most obvious that comes to mind is passing the data from the parent to the child using props and the other way around with emitting events.

But there’s a way that combines both of those and that’s custom inputs using v-model.

<ItemForm v-model="item" />

Enter fullscreen mode

Exit fullscreen mode

is the same as

<ItemForm v-bind:value="item" v-on:input="item = $event" />

Enter fullscreen mode

Exit fullscreen mode

So all we need to do is replace the prop with a v-model in the parent component and make sure we emit an input event every time one of the fields of the form changes.

Going back to the ItemAdd.vue view component, I updated it replacing the prop with a v-model:

- <ItemForm :item="item" />
+ <ItemForm v-model="item" />

Enter fullscreen mode

Exit fullscreen mode

Finally, in the ItemForm.vue component we need to clone the value property into the local data property called item, update it whenever there’s an input event in the form fields and emit it back to the parent:

- <v-text-field v-model="item.name" label="Item name" />
+ <v-text-field :value="value.name" @input="nameChanged($event)" label="Item name" />

...

 <script>
 export default {
   props: [
-    'item',
+    'value',
     'allFlatmates'
   ],
   data () {
     return {
+      item: {},
     }
   },
+  watch: {
+    value (newValue) {
+      if (Object.keys(this.item).length === 0 && this.item.constructor === Object) {
+       this.item = newValue
+      }
+    },
+  },
+  created () {
+    this.item = { ...this.value }
+  },
+  methods: {
+    nameChanged ($event) {
+      this.item.name = $event
+      this.$emit('input', this.item)
+    },
+  }

Enter fullscreen mode

Exit fullscreen mode

In case you are wondering about the watcher, I had to create it because the value prop might not yet have a value when the component is created since it has to come asynchronously from an API.

And that’s it! So many fewer lines of code, so many fewer things to worry about.

Conclusion

Even if you are using Vuex for state management in a project, it should not be considered the only way of communicating data amongst components. This use-case demonstrates the simplicity of using Vue’s default features.

I hope this post helps somebody!

Posted By: Anonymous

I’m making a todo app in vue.js which has a component TodoItem

<template>
  <div class="todo-item" v-bind:class="{'is-completed':todo.completed}">
    <p>
      <input type="checkbox" @change="markCompleted" />
      {{todo.task}}
      <button class="del">x</button>
    </p>
  </div>
</template>

<script>
export default {
  name: "TodoItem",
  props: ["todo"],
  methods: {
    markCompleted() {
      this.todo.completed = true
    },
  },
};
</script>

todo prop that I’m passing:

{
  id:1,
  task:'todo 1',
  completed:false
}

but it is throwing an error error Unexpected mutation of «todo» prop

Solution

Method 1 (Vue 2.3.0+) – From your parent component, you can pass prop with sync modifier

Parent Component

<TodoItem v-for="todo in todoList" :key="todo.id" todo_prop.sync="todo">

Child Component

    <template>
      <div class="todo-item" v-bind:class="{'is-completed':todo.completed}">
        <p>
          <input type="checkbox" @change="markCompleted" />
          {{todo.task}}
          <button class="del">x</button>
        </p>
      </div>
    </template>
    
    <script>
    export default {
      name: "TodoItem",
      props: ["todo_prop"],
      data() {
         return {
            todo: this.todo_prop
         }
      },
      methods: {
        markCompleted() {
          this.todo.completed = true
        },
      },
    };
    </script>

Method 2 – Pass props from parent component without sync modifier and emit an event when the value changed. For this method, everything else is similar as well. Just need to emit an event when the todo item changed to completed.

The code is untested. Apologies if anything does not work.

Answered By: Anonymous

Related Articles

  • Is CSS Turing complete?
  • How to properly do JSON API GET requests and assign output…
  • How to parse JSON with XE2 dbxJSON
  • Azure Availability Zone ARM Config
  • The ‘compilation’ argument must be an instance of…
  • Search match multiple values in single field in…
  • Event Snippet for Google only shows one event while testing…
  • Avoid creating new session on each axios request laravel
  • How do I limit the number of digits from 6 to 4 in JS?…
  • Why does this Azure Resource Manager Template fail…

Disclaimer: This content is shared under creative common license cc-by-sa 3.0. It is generated from StackExchange Website Network.

Понравилась статья? Поделить с друзьями:
  • Error unexpected end of file radmir
  • Error uncaught rangeerror webassembly memory could not allocate memory
  • Error unauthorized authentication required docker
  • Failed to execute script get как исправить
  • Failed to establish a connection unknown win32 error 10060