Vuejs editing prop object property in child component - vue.js

I have been using Vue for a while but still do not have a clear understanding of the benefits related to this (probably controversial) question. I know it has been asked in different ways many times, but I can't find a clear answer.
As "Example 1", let's say I have a parent component that contains an address object, and it passes that address to a child AddressForm so it can be edited by the user, like this:
// Parent.vue
<template>
<AddressForm :address="address" #submit="onSubmit"/>
</template>
<script>
export default {
data () {
return {
address: {}
}
},
methods: {
onSubmit() {
console.log('Submit', this.address);
}
}
}
</script>
Then, the child component looks like this, which directly manipulates the properties of the parent address object.
Here I am only showing an input for the address.name to keep things concise, but you can imagine there is a similar text input for any other properties of the address.
// AddressForm.vue
<template>
<form #submit.prevent="onSubmit">
<input v-model="address.name">
<button type="submit">Submit</button>
</form>
</template>
<script>
export default {
props: {
address: {
type: Object,
required: true,
}
},
methods: {
this.$emit('submit');
}
}
</script>
The issues here is that I am directly editing the address prop in the child. Of course I do not get any warnings from Vue about it because I am only editing a property of the address object, not actually mutating it by reassigning.
There are countless places where I can read about why this is not best practice, it's an anti-pattern, and it's a bad idea. You shouldn't alter props in child components.
Ok, but why? Can someone provide any type of real world use-case where the code above can lead to issues down the road?
When I read about the "proper" alternative ways to do things, I am even more confused because I don't see any real difference. Here's what I mean:
Let's call this "Example 2", where I now use v-model to enforce "proper" 2-way binding rather than editing the prop directly like I was in Example 1:
// Parent.vue
<template>
<AddressForm v-model="address" #submit="onSubmit"/>
</template>
<script>
export default {
data () {
return {
address: {}
}
},
methods: {
onSubmit() {
console.log('Submit', this.address);
}
}
}
</script>
// AddressForm.vue
<template>
<form #submit.prevent="onSubmit">
<input v-model="localValue.name">
<button type="submit">Submit</button>
</form>
</template>
<script>
export default {
props: {
value: {
type: Object,
required: true,
}
},
computed: {
localValue: {
get: function() {
return this.value;
}
set: function(value) {
this.$emit('input', value);
}
}
},
methods: {
this.$emit('submit');
}
}
</script>
What is the difference between Example 1 and Example 2? Aren't they doing, literally, exactly the same thing?
In Example 2, the computed property says that localValue is always equal to value. Meaning, they are the same exact object, just like they were in Example 1.
Further, as you type in the input, if you watch the events being fired in the Vue debugger, AddressForm never even emits the input events, presumably because localValue isn't actually being set, since it's just an object property that's being changed.
This again shows that Example 1 and Example 2 are doing the exact same thing. The object property is still being directly mutated within the child even though v-model is being used.
So, again, my question is, why is Example 1 considered bad practice? And how is Example 2 any different?

Referring to the docs, manipulating a prop directly causes several issues:
Makes app's data flow harder to understand
Every time a parent component is updated, all props in the child component will be refreshed

Related

Vue prop watcher triggered unexpectedly if the prop is assigned to an object directly in template

Here is the code:
<template>
<div>
<foo :setting="{ value: 'hello' }" />
<button #click="() => (clicked++)">{{ clicked }}</button>
</div>
</template>
<script>
import Vue from 'vue'
// dummy component with dummy prop
Vue.component('foo', {
props: {
setting: {
type: Object,
default: () => ({}),
},
},
watch: {
setting() {
console.log('watch:setting')
},
},
render(createElement) {
return createElement(
'div',
['Nothing']
)
},
})
export default {
data() {
return {
clicked: 0,
}
},
}
</script>
I also made a codepen: https://codepen.io/clinicion-lin/pen/LYNJwJP
The thing is: every time I click the button, the watcher for setting prop in foo component would trigger, I guess the cause is that the content in :settings was re-evaluated during re-render, but I am not sure.
This behavior itself does not cause any problem but it might cause unwanted updates or even bugs if not paid enough attention (actually I just made one, that's why I come to ask :D). The issue can be easily resolved by using :setting="settingValue", but I am wondering if there are alternative solutions, OR best practices for it. I saw some code are assigning object in template directly too, and it feels natural and convenient.
Thanks for anyone who can give an explanation or hint.
First, the docs: "every time the parent component is updated, all props in the child component will be refreshed with the latest value"
By using v-bind:propA="XX" in the template, you are telling Vue "XX is JavaScript expression and I want to use it as value of propA"
So if you use { value: 'hello' } expression, which is literally "create new object" expression, is it really that surprising new object is created (and in your case watcher executed) every time parent is re-rendered ?
To better understand Vue, it really helps to remember that everything in the template is always compiled into plain JavaScript and use the tool as vue-compiler-online to take a look at the output of Vue compiler.
For example template like this:
<foo :settings="{ value: 'hello' }" />
is compiled into this:
function render() {
with(this) {
return _c('foo', {
attrs: {
"settings": {
value: 'hello'
}
}
})
}
}
Template like this:
<foo :settings="dataMemberOrComputed" />
is compiled into this:
function render() {
with(this) {
return _c('foo', {
attrs: {
"settings": dataMemberOrComputed
}
})
}
}
It's suddenly very clear new object is created every time in the first example, and same reference to an object (same or different ...depends on the logic) is used in second example.
I saw some code are assigning object in template directly too, and it feels natural and convenient.
I'm still a Vue newbie but I browse source code of "big" libraries like Vuetify from time to time to learn something new and I don't think passing "new object" expression into prop is common pattern (how that would be useful?).
What is very common is <foo v-bind="{ propA: propAvalue, propB: propBValue }"> - Passing the Properties of an Object which is a very different thing...

Vue: Make sure the component cannot modify props (even if it is a reference object)?

I know that the vue model is a unidirectional data flow of props.
However, when prop is a reference object, the component can directly modify its properties. This is wrong, but vue will not check it.
I hope there is a mechanism to ensure that the component cannot modify the props (even if it is a reference object), rather than being checked by the developer.
For example, I have a component
<template>
<input v-model="obj.text" />
</template>
<script>
export default {
props: ['obj']
};
</script>
And a page that uses it
<template>
<my-template :obj="myobj"></my-template>
</template>
<script>
export default {
data() {
myobj: {
text: "hello";
}
}
};
</script>
When data changes in 'input', myobj.text will change together. This violates the unidirectional data flow.
Of course, as shown in the answer, I can use the "get" and "set" methods of the "computed".
But I must be careful not to write 'obj.someProperty' to any 'v-model', but this requires my own attention.
I hope there is a mechanism to give a hint when I make a mistake.
Couldn't find an existing duplicate so here's an answer. If anyone can find one, let me know and I'll make this one a Community wiki.
Use a computed property with getter and setter to represent your v-model value.
The getter gets the value from the prop and the setter emits the new value to the parent.
For example
<input v-model="computedProp">
props: ['referenceObject'],
computed: {
computedProp: {
get () {
return this.referenceObject.someProperty
},
set (val) {
this.$emit('updated', val)
}
}
}
and in the parent
<SomeComponent :reference-object="refObject" #updated="updateRefObject">
data: () => ({ refObject: { someProperty: 'initial value' } }),
methods: {
updateRefObject (newVal) {
this.refObject.someProperty = newVal
}
}

VueJS: Is it really bad to mutate a prop directly even if I want it to ovewrite its value everytime it re-renders?

The question says it all. As an example, think of a component that can send messages, but depending on where you call this component, you can send a predefined message or edit it. So you would end with something like this:
export default {
props: {
message: {
type: String,
default: ''
}
},
methods: {
send() { insert some nice sending logic here }
}
}
<template>
<div>
<input v-model="message"></input>
<button #click="send">Send</button>
</div>
</template>
If I do this and try to edit the predefined message then Vue warns me to "Avoid mutating a prop directly since the value will be overwritten whenever the parent component re-renders", but that's exactly the behaviour I'm searching for as the predefined message should return to being unedited if the user closes the component and opens it again.
I'm also not passing the prop to the father component, so the sending logic itself can be included in this same component.
It would still be considered bad practice? Why? How can I make it better? Thanks in advance!
A solution would be to assign the message you are passing as a prop to a variable in data and set this variable to the v-model instead.
<template>
<div>
<input v-model="message"></input>
<button #click="send">Send</button>
</div>
</template>
<script>
export default {
data(){
return{ message:this.msg
}
},
props: {
msg: {
type: String,
default: ''
}
},
methods: {
send() { use a bus to send yout message to other component }
}
}
</script>
If you are not passing the data to another component or from a component, you shouldn't be using props, you should use Vue's data object and data binding. This is for any component data that stays within itself, the component's local state. This can be mutated by you as well so for our example I would do something like:
export default {
data: function () {
return {
message: '',
}
},
methods: {
send() {
// insert some nice sending logic here
// when done reset the data field
this.data.message = '';
}
}
}
<template>
<div>
<input>{{ message }}</input>
<button #click="send">Send</button>
</div>
</template>
More info on props vs data with Vue

VueJS functional components (SFC): how to encapsulate code?

I wrote a simple template-substitution component in VueJS as a single-file component. It doesn't have many features: just one prop, and I also made a computed property to encapsulate some tricky transformations that are done to that prop before it can be used in the template. It looks something like the following:
<template>
...some-html-here...
<a :href="myHref">...</a>
...
</template>
<script>
export default {
name: 'MyComponent',
props: {
href: { type: String, required: true },
},
computed: {
myHref() {
let result = this.href;
// several lines of complicated logic making substitutions and stuff
// ...
return result;
}
}
};
</script>
Now I think this should really be a functional component, as it has no state, no data, no reactivity, and so lunking around a whole instance is wasteful.
I can make this functional just by adding the 'functional' attribute to my <template>. In a functional component, of course, there are no such things as computed properties or methods or whatever. So my question is: where can I put my several lines of complicated logic? I don't want to have to embed this directly into my template, especially as it is used in multiple places. So where can I put code to transform my input props and make them ready to use in my template?
Great question.I was trying to find the same answer and i ended up with the following which i don't know if it is a good way to do though.
The "html" part:
<template functional>
<div>
<button #click="props.methods.firstMethod">Console Something</button>
<button #click="props.methods.secondMethod">Alert Something</button>
</div>
</template>
The "js" part:
<script>
export default {
props: {
methods: {
type: Object,
default() {
return {
firstMethod() {
console.log('You clicked me')
},
secondMethod() {
alert('You clicked me')
}
}
}
}
}
}
</script>
See it in action here
Make sure to read about functional components at docs
NOTE: Be aware using this approach since functional components are stateless (no reactive data) and instanceless (no this context).

2-way binding in Vue 2.3 component

I understand the .sync modifier returned in Vue 2.3, and am using it for a simple child component which implements a 'multiple-choice' question and answer. The parent component calls the child like this:
<question
:stem="What is your favourite colour?"
:options="['Blue', 'No, wait, aaaaargh!']
:answer.sync="userChoice"
>
The parent has a string data element userChoice to store the result from the child component. The child presents the question and radio buttons for the options. The essential bits of the child look like this (I'm using Quasar, hence q-radio):
<template>
<div>
<h5>{{stem}}</h5>
<div class="option" v-for="opt in options">
<label >
<q-radio v-model="option" :val="opt.val" #input="handleInput"></q-radio>
{{opt.text}}
</label>
</div>
</div>
</template>
export default {
props: {
stem: String,
options: Array,
answer: String
},
data: () => ({
option: null
}),
methods: {
handleInput () {
this.$emit('update:answer', this.option)
}
}
}
This is all working fine, apart from the fact that if the parent then changes the value of userChoice due to something else happening in the app, the child doesn't update the radio buttons. I had to include this watch in the child:
watch: {
answer () {
this.option = this.answer
}
}
But it feels a little redundant, and I was worried that emitting the event to update the parent's data would in fact cause the child 'watch' event to also fire. In this case it would have no effect other than wasting a few cycles, but if it was logging or counting anything, that would be a false positive...
Maybe that is the correct solution for true 2-way binding (i.e. dynamic Parent → Child, as well as Child → Parent). Did I miss something about how to connect the 'in' and 'out' data on both sides?
In case you're wondering, the most common case of the parent wanting to change 'userChoice' would be in response to a 'Clear Answers' button which would set userChoice back to an empty string. That should have the effect of 'unsetting' all the radio buttons.
Your construction had some oddities that didn't work, but basically answer.sync works if you propagate it down to the q-radio component where the changing happens. Changing the answer in the parent is handled properly, but to clear values, it seems you need to set it to an object rather than null (I think this is because it needs to be assignable).
Update
Your setup of options is a notable thing that didn't work.
I use answer in the q-radio to control its checked state (v-model has special behavior in a radio, which is why I use value in conjunction with v-model). From your comment, it looks like q-radio wants to have a value it can set. You ought to be able to do that with a computed based on answer, which you would use instead of your option data item: the get returns answer, and the set does the emit. I have updated my snippet to use the val prop for q-radio plus the computed I describe. The proxyAnswer emits an update event, which is what the .sync modifier wants. I also implemented q-radio using a proxy computed, but that's just to get the behavior that should already be baked-into your q-radio.
(What I describe is effectively what you're doing with a data item and a watcher, but a computed is a nicer way to encapsulate that).
new Vue({
el: '#app',
data: {
userChoice: null,
options: ['Blue', 'No, wait, aaaaargh!'].map(v => ({
value: v,
text: v
}))
},
components: {
question: {
props: {
stem: String,
options: Array,
answer: String
},
computed: {
proxyAnswer: {
get() {
return this.answer;
},
set(newValue) {
this.$emit('update:answer', newValue);
}
}
},
components: {
qRadio: {
props: ['value', 'val'],
computed: {
proxyValue: {
get() {
return this.value;
},
set(newValue) {
this.$emit('input', newValue);
}
}
}
}
}
}
},
methods: {
clearSelection() {
this.userChoice = {};
}
}
});
<script src="//cdnjs.cloudflare.com/ajax/libs/vue/2.3.3/vue.min.js"></script>
<div id="app">
<question stem="What is your favourite colour?" :options="options" :answer.sync="userChoice" inline-template>
<div>
<h5>{{stem}}</h5>
<div class="option" v-for="opt in options">
<div>Answer={{answer && answer.text}}, option={{opt.text}}</div>
<label>
<q-radio :val="opt" v-model="proxyAnswer" inline-template>
<input type="radio" :value="val" v-model="proxyValue">
</q-radio>
{{opt.text}}
</label>
</div>
</div>
</question>
<button #click="clearSelection">Clear</button>
</div>