Vue Error: Avoid Mutating a Prop Directly

You probably found this article because you've gotten this confusing error:

Error: Unexpected mutation of prop

In Vue 2 you'd get this error message:

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.

Here's the fix:

Do not update or override the value of prop. Instead, use refs or computed refs to create new state that's based on the prop.

If you read further, I'll show you a simple pattern you can use to fix this error — and never see it again.

By the end of this article you'll learn:

  • A simple pattern for fixing this issue
  • What this error means, and what causes it
  • Why mutating props is an anti-pattern
  • How to avoid this when using v-model

How to fix the "unexpected mutation of prop" error

The docs also explain what's going on here.

This error is caused by taking a prop that is passed in by the parent component, and then changing that value.

There are many situations where we need to take the prop that we are passed, and then do something extra with it.

Maybe you need to take a list and sort it, or filter it.

Maybe it's taking some numbers and summing them together, or doing some other calculation with them.

Well, we still don't mutate the prop.

Instead, you can use the always useful computed ref to solve the same problem.

Simple example

We'll start with a simple example, and then move on to something a little more interesting.

In our ReversedList component we will take in a list, reverse it, and render it to the page:

ReversedList.vue
1
2
3
4
5
6
7
8
9
10
11
12
<template>
<ul>
<li v-for="item in list" />
</ul>
</template>
<script setup>
const props = defineProps({
list: Array,
});
props.list = props.list.toReversed();
</script>

This is an example of a component that, although functional, isn't written very well.

In fact, it isn't completely functional either.

It will only reverse the initial list it is given. If the prop is ever updated with a new list, that won't be reversed 😕.

But, we can use a computed ref to clean this component up!

ReversedList.vue
1
2
3
4
5
6
7
8
9
10
11
12
13
14
<template>
<ul>
<li v-for="item in reversedList" />
</ul>
</template>
<script setup>
import { computed } from 'vue';
const props = defineProps({
list: Array,
});
const reversedList = computed(() => props.list.toReversed());
</script>

We setup the computed ref reversedList, and then swap that out in our li tag.

The functionality is also fixed now, as reversedList will be recomputed every time that list is updated.

We're just getting started though. Let's move on to something a little more complicated.

The (more) complicated example

Okay, so you might have already known to use computed refs like we just showed.

But what if you don't always want to use the computed value?

How do we switch between using the prop passed in from the parent, and using the computed ref?

Let's expand our example.

Now we will have a button in our component that will toggle reversing the list. This way we will be switching between using the list as-is from the parent, and using the computed reversed list:

ReversedList.vue
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
<template>
<div>
<!-- Add in a button that toggles our `reversed` flag -->
<button @click="reversed = !reversed">Toggle</button>
<ul>
<li v-for="item in reversedList" />
</ul>
</div>
</template>
<script setup>
import { ref, computed } from 'vue';
const props = defineProps({
list: Array,
});
// Keep track of our reversed state
const reversed = ref(false);
// Return a different list based on our state and prop
const reversedList = computed(() => {
if (reversed.value) {
return props.list.toReversed()
}
return props.list;
});
</script>

First, we define a ref called reversed, which keeps track of whether or not we should be showing the reversed list.

Second, we add in a button. When the button is clicked, we toggle reversed between true and false.

Third, we update our computed ref reversedList to also rely on our reversed flag. Based on this flag we can decide to reverse the list, or just use what was passed in as a prop.

Here we see a bit more of the power and flexibility of computed refs.

We don't have to tell Vue that we need to update reversedList when either reversed or list change, it just knows.

Mutating props in Vue is an anti-pattern

Yes, in Vue, mutating props like this is considered to be an anti-pattern.

Meaning — please don't do it, or you'll cause a lot of headaches for yourself.

Why an anti-pattern?

In Vue, we pass data down the the component tree using props. A parent component will use props to pass data down to it's children components. Those components in turn pass data down another layer, and so on.

Then, to pass data back up the component tree, we use events.

We do this because it ensures that each component is isolated from each other. From this we can guarantee a few things that help us in thinking about our components:

  • Only the component can change it's own state
  • Only the parent of the component can change the props

If we start seeing some weird behaviour, knowing with 100% certainty where these changes are coming from makes it much easier to track down.

Keeping these rules makes our components simpler and easier to reason about.

But if we mutate props, we are breaking these rules!

Props are overwritten when re-rendering

There is another thing to keep in mind.

When Vue re-renders your component — which happens every time something changes — it will overwrite any changes you have made to your props.

This means that even if you try to mutate the prop locally, Vue will keep overwriting those changes.

Not a very good strategy, even if you don't think that this is an anti-pattern.

Conclusion

Now you know why mutating props isn't a good idea, as well as how to use computed refs instead.

In my opinion, computed refs are one of the most useful features of Vue. You can do a ton of very useful patterns with them.

If this still didn't fix your problem, or you have questions about this article, please reach out to me on Twitter.

I'm always available to help!