5
votes

Vue.js displays warning when we try to change the prop value directly, like this:

Vue.component('Games', {
    template: `
        <div>
            <ol>
                <li v-for="game in games">{{ game }}</li>
            </ol>
            <button @click="clear">Clear games</button>
        </div>
    `,
    props: ['games'],
    methods: {
        clear: function () {
            this.games = [];
        }
    }
});

The warning being displayed is:

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.

I know why that happens, but what I find surprising is that it doesn't happen with .push(). If I change the method to add a value to the array instead of rewriting it, there's no warning:

methods: {
    add: function () {
        this.games.push('Whatever');
    }
}

Why is there no warning? How is pushing directly to the prop fine and rewriting is not?

3
The prop being pushed to is an array. After pushing a new value, it is still an array. I believe vue doesn't do a deep watch on props out of the box (i.e, it doesn't care about what's inside the array). - Lewis
How does it matter that it's of the same type? Vue.js even has its own wrapper for Array.push from what I know, so it would make things even easier to control. Changing the props is said to be wrong in case of re-rendering. It's not about the type, but about the value being different than a value being passed in a prop. - Robo Robok
I'll post a more in depth answer now. - Lewis

3 Answers

3
votes

It's just because Array is kind of reference memory. When you have Array or Object stored in any variable it's a reference variable.

Memory management in such case, The reference variable will points to a memory address in heap so you can add more n more value to address. However you cannot just replace that address with any new value even with the new address.

1
votes

The prop being pushed to is an array. After pushing a new value, it is still an array. I believe vue doesn't do a deep watch on props out of the box (i.e, it doesn't care about what's inside the array).

To quote this article;

const array = [1, 2, 3, 4];
// array = [1, 2, 3, 4]

Now you update the array by pushing some more values into it:

array.push(5);
array.push(6);
array.push(7);
// array = [1, 2, 3, 4, 5, 6, 7]

Here's the question: has array changed?

Well, it's not that simple.

The contents of array have changed, but the variable array still points to the same Array object. The array container hasn't changed, but what is inside of the array has changed.

So when you watch an array or an object, Vue has no idea that you've changed what's inside that prop. You have to tell Vue that you want it to inspect inside of the prop when watching for changes.

Article By Michael Thiessen, Posted Oct 2018 - All credit to them.

In response to your comment about this still being an anti-pattern, I'd say this;

If we think about why mutating props on a component directly is an anti-pattern in the first place, the reasoning still stands. To quote Michael again (I keep stumbling across his stuff by accident, I promise);

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

1
votes

As the warning says, one results in unexpected behaviour

When a component is updated/rendered, the props are written as values to the underlying component model. This can happen again at "any" time. This will often happen with components rendered in v-if or v-for conditions. In this case the prop value is written again from the parent to the child.

If the child increases the counter it will only increase it's local copy of the prop, the original value parent.data.counter will still remain at the value 5. If the parent is updated (for example by setting counter=counter-1) then Vue will override the value in the child with the new value from the parent. So all changes of the child are lost - and this can lead to unexpected behaviour.

This problem will not happen, if the prop is a reference, which is mutated

If the prop is an array, there exists only a single copy of this array in memory. Any mutation of the array will change the original array in parent as well as in child. This will only work for mutations of the array itself, if you would e.g. try child.propArray = child.propArray.slice(3) you will have the same problems as before. Since slice does not change the original array, but creates a copy the child will have a different reference than the parent and unexpected behaviour will likely manifest.

Here is a code-fiddle demonstrating the issues:

Vue.component('child-comp', {
  // camelCase in JavaScript
  props: ['counter', 'arr'],
  template: `<h3>Child Counter: {{ counter }} <button @click="inc">+</button>
     -- Array: {{arr}} <button @click="push">push</button></h3></h3>`,
  methods: { inc() { this.counter++ }, push() { this.arr.push(this.arr.length+1) } }
})

new Vue({
  el: '#main',
  data: { counter: 1, arr: [] },
  methods: { inc() { this.counter++ }, push() { this.arr.push(this.arr.length+1) } }
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<main id="main">
   <h3>Main Counter: {{counter}} <button @click="inc">+</button>
     -- Array: {{arr}} <button @click="push">push</button></h3>
   <child-comp :counter="counter" :arr="arr"></child-comp>
   <child-comp :counter="counter" :arr="arr"></child-comp>
</main>