Vue 3 why is the parent updated when child data changes? - vue.js

With this parent...
<template>
<h2>Parent</h2>
{{ parent.a }}
{{ parent.b }}
<ChildComponent :data="parent" />
</template>
<script setup>
import { reactive } from 'vue'
import ChildComponent from './components/ChildComponent.vue'
const parent = reactive({ a: 1, b: 2 })
</script>
And this child...
<template>
<h2>Child component</h2>
<p>{{ child.a }}</p>
<p>{{ child.b }}</p>
<input type="text" v-model="child.b" />
</template>
<script setup>
import { reactive } from 'vue'
const props = defineProps(['data'])
const child = reactive(props.data)
child.a = 'why do i update the parent?'
</script>
Why is the data on the parent being updated here? I thought that with binding of the 'data' prop being one-way, I would need an emit to send the data back to the parent? Instead any changes to the child object in the child component is updating the parent object in the parent.
In the documentation it says
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 in JavaScript objects and arrays are passed by reference, and it is unreasonably expensive for Vue to prevent such mutations.
But from my example, a and b aren't nested?

Further reading and I've found that it's the use of reactive on the child that is the issue. It is creating a reactive copy of the original (a reference), so any updates to the copy were affecting both. I needed to use ref instead:
<template>
<div>
<h2>Child component</h2>
<p>{{ a }}</p>
<p>{{ b }}</p>
<input type="text" v-model="b" />
</div>
</template>
<script setup>
import { ref } from 'vue'
const props = defineProps(['data'])
const a = ref(props.data.a)
const b = ref(props.data.b)
</script>

The reason is that JS treats an object like a reference. A more accurate term is call-by-sharing.
So when you modify a props object inside a child component, you actually modify the same object of the parent component regardless you are using reactive or not
Let's consider this code snippet:
<template>
<h2>Child component</h2>
<p>{{ child.a }}</p>
<p>{{ child.b }}</p>
<input type="text" v-model="child.b" />
</template>
<script setup>
import { reactive } from 'vue'
const props = defineProps(['data'])
const child = props.data // <--- No need to use reactive here
child.a = 'why do i update the parent?'
</script>
The data of the parent component will be still updated along with the child component regardless of the use of reactive. So, to avoid side effects, Vue recommended never mutating props directly. You should use event emitter instead

The reason why child component is updating parent values is the two way data binding. You can solve this problem by creating a copy of an object inside a child component and then use the copied version.

Related

How to declare local property from composition API in Vue 3?

In Vue 2 I would do this:
<script>
export default {
props: ['initialCounter'],
data() {
return { counter: this.initialCounter }
}
}
</script>
In Vue 3 I tried this:
<script setup>
import { ref } from 'vue';
defineProps({ 'initialCounter': Number })
const counter = ref(props.initialCounter)
</script>
This obviously doesn't work because props is undefined.
How can I bind one-way properties to a local variable in Vue 3?
It seems the result of defineProps is not assigned as a variable. check Vue3 official doc on defineProps. Not really sure what is the use case of ref() here but toRef API can be used as well.
import { ref } from 'vue';
const props = defineProps({ 'initialCounter': Number })
const counter = ref(props.initialCounter)
Retrieving a read-only value and assigning it to another one is a bad practice if your component is not a form but for example styled input. You have an answer to your question, but I want you to point out that the better way to change props value is to emit update:modalValue of parent v-model passed to child component.
And this is how you can use it:
<template>
<div>
<label>{{ label }}</label>
<input v-bind="$attrs" :placeholder="label" :value="modelValue" #input="$emit('update:modelValue', $event.target.value)">
<span v-for="item of errors">{{ item.value }}</span>
</div>
</template>
<script setup>
defineProps({ label: String, modelValue: String, errors: Array })
defineEmits(['update:modelValue'])
</script>
v-bind="$attrs" point where passed attributes need to be assigned. Like type="email" attribute/property of a DOM element.
And parent an email field:
<BaseInput type="email" v-model="formData.email" :label="$t('sign.email')" :errors="formErrors.email" #focusout="validate('email')"/>
In this approach, formdata.email in parent will be dynamically updated.

How can I reassign and repass props data with keeping its reactivity?

<template>
<p>
<input type="text" v-model="appInput">
{{ appInput }}
</p>
<ParentComponent :appInput='appInput' :appObject='appObject'/>
</template>
<script setup>
import ParentComponent from './components/ParentComponent.vue'
import { ref } from 'vue'
const appInput = ref('')
const appObject = ref({
text1: 'text1',
text2: 'text2'
})
</script>
<!-- ------------------------------------- -->
<template>
<!-- {{ appInput }} -->
{{ props.appInput }}
<!-- {{ appObject }} -->
{{ props.appObject.text1 }}
<ChildComponentVue :appInput='appInput'/>
</template>
<script setup>
import { defineProps } from 'vue';
import ChildComponentVue from './ChildComponent.vue';
const props = defineProps(['appInput', 'appObject'])
// const appInput = props.appInput
// const appObject = props.appObject
</script>
Using Vue 3, I want to reactively render text from App.vue on ParentComponent.vue and ChildComponent.vue by passing data from App to Parent, and Parent to child successively.
But when I reassign props data on ParentComponent for use it conveniently, it lose reactivity.
I tried repack it with ref() on ParentComponent, but as Vue3 Official Document says, it doesn't work.
But it is too dirty to use passed data without reassign in template (like {{ props.appObject.text1 }}) and too hard to repass not entire but just some part of passed data.
and here are my questions
Are there some ways to use and repass passed data more concisely without losing its reactivity?
What is the convension on Vue3 to deal with this kind of problem happen. Just using Vuex?

Does defineProps in script setup automatically create a local property of the defined prop?

When we pass a prop to a component and define that prop from child component with defineProps a property somehow is created and accessible from child components template.
parentComponent.vue
<template>
<child-component v-model="product">
</template>
<script setup>
import childComponent from "./childComponent.vue"
</script>
childComponent.vue
<template>
{{ product }}
</template>
<script setup>
const props = defineProps(['product'])
</script>
Here in childComponents template, the product can be accessed without needing to use props.product or toRef it. I know that script setup automatically injects the used props but I could not find any info (in docs) that the defineProps does some too. Is there any info about that.
According to this section :
The script is pre-processed and used as the component's setup() function, which means it will be executed for each instance of the component. Top-level bindings in <script setup> are automatically exposed to the template. For more details
Knowing that props are unwrapped directly inside the template and also the refs are used without .value.
If you want to reference some prop inside the script you should use props.product like in this example :
<script setup>
const props = defineProps(['product'])
const total=computed(()=>props.product.quantity*props.product.unity_price))
</script>
if the prop is only accessed by template you could get rid off const props just call the macro defineProps :
<template>
{{ product }}
</template>
<script setup>
defineProps(['product'])
</script>

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

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... :(
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>

How to get child component's data in VueJs?

This is an easy question but I dont know the best way to do it correctly with Vue2:
Parent Component:
<template>
<div class="parent">
<child></child>
{{value}} //How to get it
</div>
</template>
Child Component:
<template>
<div class="child">
<p>This {{value}} is 123</p>
</div>
</template>
<script>
export default {
data: function () {
return { value: 123 }
}
}
</script>
Some ways you can achieve this include:
You can access the child VM directly via a ref, but this gets hairy because the ref won't be populated until the child component has mounted, and $refs isn't reactive, meaning you'd have to do something hacky like this:
<template>
<div class="parent">
<child ref="child"></child>
<template v-if="mounted">{{ $refs.child.value }}</template>
</div>
</template>
data() {
return {
mounted: false
}
},
mounted() {
this.mounted = true;
}
$emit the value from within the child component so that the parent can obtain the value when it is ready or whenever it changes.
Use Vuex (or similar principles) to share state across components.
Redesign the components so that the data you want is owned by the parent component and is passed down to the child via a prop binding.
(Advanced) Use portal-vue plugin when you want to have fragments of a component's template to exist elsewhere in the DOM.
Child components pass data back up the hierarchy by emitting events.
For example, in the parent, listen for an event and add the child value to a local one (an array for multiple child elements would be prudent), eg
<child #ready="childVal => { value.push(childVal) }"></child>
and in the child component, $emit the event on creation / mounting, eg
mounted () {
this.$emit('ready', this.value)
}
Demo ~ https://jsfiddle.net/p2jojsrn/