Javascript is required
·
4 min read
·
8108 views

Vue Tip: Avoid Mutating a Prop Directly

Vue Tip: Avoid Mutating a Prop Directly Image

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

ESLint 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.

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:

TodoList.vue
1<script setup lang="ts">
2import { ref, Ref } from 'vue'
3import TodoItem from './TodoItem.vue'
4
5const todos: Ref<Array<{ id: number; name: string; completed: boolean }>> = ref([
6  { id: 1, name: 'Buy milk', completed: false },
7  { id: 2, name: 'Clean house', completed: false },
8])
9</script>
10
11<template>
12  <h2>Todo</h2>
13  {{ todos }}
14  <div v-if="todos.length > 0" class="todo-list">
15    <TodoItem v-for="todoItem of todos" :key="todoItem.id" :todo="todoItem" />
16  </div>
17  <span v-else>Nothing todo</span>
18</template>
19
20<style scoped>
21.todo-list {
22  display: flex;
23  flex-direction: column;
24}
25</style>

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

TodoItem.vue
1<script setup lang="ts">
2defineProps<{
3  todo: { id: number; name: string; completed: boolean }
4}>()
5</script>
6
7<template>
8  <div class="container">
9    <p>{{ todo.name }}</p>
10    Completed?
11    <input v-model="todo.completed" type="checkbox" />
12  </div>
13</template>
14
15<style scoped>
16.container {
17  border: 1px solid white;
18  border-radius: 10px;
19  padding: 10px;
20}
21</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.

Warning

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

Info

In most cases, the error can be solved using a computed property.

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 accesses the prop's value, and the setter emits an event to the parent:

TodoItem.vue
1<script setup lang="ts">
2import { computed } from 'vue'
3
4const props = defineProps<{
5  todo: { id: number; name: string; completed: boolean }
6}>()
7
8const emit = defineEmits<{
9  (e: 'update-completed', value: boolean): void
10}>()
11
12const completedInputModel = computed({
13  // getter
14  get() {
15    return props.todo.completed
16  },
17  // setter
18  set(newValue: boolean) {
19    emit('update-completed', newValue)
20  },
21})
22</script>
23
24<template>
25  <div class="container">
26    <p>{{ todo.name }}</p>
27    Completed?
28    <input v-model="completedInputModel" type="checkbox" />
29  </div>
30</template>
31
32<style scoped>
33.container {
34  border: 1px solid white;
35  border-radius: 10px;
36  padding: 10px;
37}
38</style>

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

TodoList.vue
1<script setup lang="ts">
2import { ref, Ref } from 'vue'
3import { Todo } from './TodoItem.model'
4import TodoItem from './TodoItem.vue'
5
6const todos: Ref<Array<Todo>> = ref([
7  { id: 1, name: 'Buy milk', completed: false },
8  { id: 2, name: 'Clean house', completed: false },
9])
10
11const onUpdateCompleted = (updatedTodo: Todo) => {
12  todos.value = todos.value.map((todo) => {
13    if (todo.id === updatedTodo.id) {
14      todo = updatedTodo
15    }
16    return todo
17  })
18}
19</script>
20
21<template>
22  <h2>Todo</h2>
23  {{ todos }}
24  <div v-if="todos.length > 0" class="todo-list">
25    <TodoItem v-for="todoItem of todos" :key="todoItem.id" :todo="todoItem" @update-completed="onUpdateCompleted" />
26  </div>
27  <span v-else>Nothing todo</span>
28</template>
29
30<style scoped>
31.todo-list {
32  display: flex;
33  flex-direction: column;
34}
35</style>

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

Info

The amazing Michael Thiessen also wrote a detailed article about this topic.

Video