What EXACTLY does mutating a child via emitted event .sync modifier do?

Viewed 178

So I've been investigating how mutating child props from the parent can be done. I already encountered the pitfall with using v-model. It throws this error:

[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. Prop being mutated: "number1"

I learnt to deal with this problem by emitting events from the child and using .sync modifier in the parent.

However, I still feel like I didnt really understand whats going on under the hood in both cases. As far as I understood, when mutating the childs properties from parent with v-model, the mutated data also mutates for any other parent importing the child.

To test this out, I built the following setup:

I have a child component with two inputs: One using v-model, the other an emitted event to mutate the childs props. We call it "compA":

<template>
  <div>
    <input
    :value="number1"
    @change="$emit('update:number1', $event.target.value)"
    placeholder="number1emittedEvent"
    />
    <input v-model="number1" placeholder="number1vmodel">
  </div>
</template>

<script>
export default{
  name: "compA",
  props: {
    number1: String
  }
}
</script>

compA is being imported by compZ and compB. compZ also imports compB, and then compZ is finally being imported by myComplexView5.vue (my project uses routing).

compB:

    <template>
  <div>
    <h1>compB containing compA</h1>
    <compA/>
    <p v-text="number1"></p>
  </div>
</template>

<script>
import compA from "@/components/complexComponent5/compA.vue"
export default {
  name: "compB",
  components: {
    compA
  },
  data(){
    return {
      number1:''
    }
  }
}

</script>

compZ:

<template>
  <div>
    <compA :number1.sync="number1"/>
    <br>
    <compB />
  </div>
</template>

<script>
import compA from "@/components/complexComponent5/compA.vue"
import compB from "@/components/complexComponent5/compB.vue"

export default {
  name: "compZ",
  components: {
    "compA" : compA,
    "compB" : compB
  },
  data(){
    return {
      number1:''
    }
  }
}
</script>

myComplexView5.vue:

<template>
  <div>
    <h1>And More testing</h1>
    <compZ />
  </div>
</template>

<script>
import compZ from "@/components/complexComponent5/compZ.vue"

export default{
  name: "myComplexView5",
  components: {
    compZ
  }
}
</script>

Now, I expected the following behavior: When inputting into the inputfield which has v-model, I'd expect the paragraph inside compB to display the changes, since its text value is bound to the number1 prop from child compA. Since I mutated the childs props directly, the changed value should show up in any of the childs parents (or grandparents and so on).

But it doesnt. And it gets even better: When I use the inputfield with the event emitting from child to parent, the paragraph inside compB receives the changes! This is basically the opposite of what I've learned from the sync modifier docs:

In some cases, we may need “two-way binding” for a prop. Unfortunately, true two-way binding can create maintenance issues, because child components can mutate the parent without the source of that mutation being obvious in both the parent and the child.

That’s why instead, we recommend emitting events in the pattern of update:myPropName. For example, in a hypothetical component with a title prop, we could communicate the intent of assigning a new value with:

this.$emit('update:title', newTitle)

Then the parent can listen to that event and update a local data property, if it wants to

Maybe this is caused by the data() inside compB which sets number1 to EMPTYSTRING when the component rerenders? I don't know, I'm very new to vue and I don't really understand when components rerender. I also used this

data() {
  return {
    number1: ''
  }
}

inside compB To prevent this error from occuring:

[Vue warn]: Property or method is not defined on the instance but referenced during render.

I don't really know how else to prevent this error from occuring, since it seems that even though compB imported the prop from compA, the prop still needs to be declared in compB Oo


EDIT: I just found out that in the code I'm using, compZ still had this paragraph element:

  <p v-text="number1"></p>

The string inputted into the inputfield appeared there, NOT in the paragraph element of compB. For some reason, even though I get no errors, neither through the emitted event from compA, nor through the v-model from compA does any change to the props inside compA show any impact inside compB... :(

1 Answers

I put your example code in a snippet at the bottom of the post so that we're on the same page about what code is actually being talked about (I removed the line with the <p v-text="number1"></p> from compB, because that component doesn't have a prop or data/computed property for number1).

Your compA component takes in a number1 prop, which it then uses for two inputs:

  • The first input uses the prop as its value and then emits an 'update:number1' event with the input's value in response to the input's change event. This allows you to specify the .sync modifier when the parent component binds a value to the compA component's number1 prop (as you do in compZ).

  • The second input directly binds the value of the number1 prop via v-model. This is not recommended (for reasons I'll explain), which is why you are seeing the "Avoid mutating a prop directly" warning. The effect of directly binding the number1 prop to this input is that whenever the value of this second input changes, the value of number1 will change, thus changing the value of the first input, and finally causing that first input to emit the update:number1 event. Technically Vue allows you to do this, but you can see how it can get confusing.

Your compB component simply renders some texts and a compA component without passing a value as a number1 prop to compA.

Your compZ component renders a compA component, binding its own number1 property value with the .sync modifier to the compA component. This compA component instance does not share any data with the compA component instance in the compB component, so we can't expect any changes to either component to affect the other.

Vue.component('compA', {
  template: `
    <div>
      <input
        :value="number1"
        @change="$emit('update:number1', $event.target.value)"
        placeholder="number1emittedEvent"
      />
      <input v-model="number1" placeholder="number1vmodel">
    </div>
  `,
  props: {
    number1: String
  }
})

Vue.component('compB', {
  template: `
    <div>
      <h1>testString</h1>
      <compA />
    </div>
  `
})

Vue.component('compZ', {
  template: `
    <div>
      <compA :number1.sync="number1"/>
      <br>
      <compB />
    </div>
  `,
  data() {
    return {
      number1: ''
    }
  }
})

new Vue({
  el: '#app'
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>

<div id="app">
  <div>
    <h1>And More testing</h1>
    <comp-z/>
  </div>
</div>

Related