Vuejs Mutating Object passed as a prop - javascript

If I'm passing (a reference to) an Object as a prop is it OK to mutate values in the prop?
I'm developing a web app which will require a lot of values to be passed to a component, and I'm trying to find the best way of passing the values to the component and back to the parent.
From everything I've read mutating a prop is the wrong way to do things, because next time the component is updated the values are passed back to the child component overwriting the mutations. But only the reference to the object is passed so any mutations to the values in the object prop happen to the original object in the parent component. Also Vuejs does not complain about mutating props when this happens.
const subComponent = {
name: "subComponent",
template: `
<div>
Sub Component Input
<input type="text" v-model="objectProp.val1"></input>
</div>`,
props: {
objectProp: {
required: false,
default: () => {return {val1: "carrot"}}
}
}
}
const aComponent = {
name: "aComponent",
template: `
<div>
val1: {{mainObject.val1}}
val2: {{mainObject.val2}}
<sub-component :objectProp="mainObject"></sub-component>
</div>`,
data: function() {
return{
mainObject: {
val1: "foo",
val2: "bar"
}
}
},
components: {
subComponent
}
}
new Vue({
el: "#app",
components: {
aComponent
}
})
Here is a JSFiddle showing an object prop being mutated.
JSFiddle

Is mutating a prop bad practice?
Yes absolutely. In more complex applications it is very easy to lose track of where/what/why is mutated
What is the right way to handle state across different components?
In small projects you can really do whatever you want, because you will most likely be able to follow the logic - even after not looking at the project for a year. The possibilities include:
Mutating Props (ugly but will work)
Using Events to mutate state in parent components; take a look at the EventBus (better)
Use global shared state; look at Vuex (best, but a little more boilerplate)
In big projects, however, you should absolutely use Vuex. It is a module for Vue that adds global shared state to your app, which you can access and mutate from all places in your app.

I think I understand what you are trying to do, You want to pass data to a child component, mutate it and then give that data back to the parent component.
You never want to mutate the props data given to the child component, however you CAN mutate a local state of the data, which could be an exact clone of the prop.
You can do this in many ways, I normally use a computed property as suggested in the Vue documentation:
https://v2.vuejs.org/v2/guide/components-props.html
in the computed return, just return the data coming in from the property.

Related

How to properly update a prop used as an initial value?

I'm wondering if I'm doing this the best way possible; I've built a component which accepts a list of items as a prop, but then I need to mutate that list internally in the component, so I followed the VueJs docs to provide the prop value as a data property's initial value, and then using the data property when I need to make any changes to the data.
export default {
props: {
items: {
type: Array,
default: () => {
return []
}
}
},
data() {
return {
itemList: this.items
}
}
}
I noticed however that this causes the itemList property to stick to the initial value, and won't update if the parent component provides a new items value to this component, this makes sense as the component was already rendered and thus it doesn't need to reevaluate all it's data properties; so to fix this I added a watcher to watch for any changes on items:
watch: {
items() {
this.itemList = this.items;
}
}
I found this other question which had a similar issue and the accepted answer did just what I just did here; however I'd like to know if this is an expected and reliable method of fixing this issue or if there are other ways to do this?
The data property uses the prop just for initialization. It won't react to prop value changes after that.
A computed property will, though. It's another way to do it, but since you're not actually doing any "computing" on the prop value, a watcher with a function that changes the data property seems more appropriate to me.
A regular watcher is shallow, though. Your suggestion shouldn't work if the parent element just mutated the array by adding or removing elements. It should only react to a reassignment of the array (I guess that's what your parent element did... replace the prop value entirely).
If you need the watcher to react to mutations to the prop as well, then you might want to create a deep watcher:
watch: {
items: {
handler(newValue) {
this.itemList = JSON.parse(JSON.stringify(newValue)); //deep array copy to prevent array mutations made by the child component from affecting the parent's array. Keep in mind this deep-copy technique using JSON has caveats.
},
deep: true
}
}
But that can become quite expensive if your array prop grows big.
Also, it seems to me you intend for the state of your component ($data.itemList) to be changed both by the component itself and by its parent *at the same time*. This can get troublesome (racing conditions and whatnot), so be careful.

Dealing with many attributes mapping to a parent component list in VueJS

I've got a list of components where I'd like them all to be editable / replicate state to the parent component.
The list component exists as:
Vue.component("shortcuts", {
props: {
shortcuts: Array
},
template: '...<shortcut-entry v-for="(shortcut, index) in shortcuts" v-bind:key="index" v-bind="shortcut" #remove="remove(index)"></shortcut-entry>...'
})
It's used with a model like this:
<shortcuts v-bind:shortcuts.sync="shortcuts"></shortcuts>
Now each shortcut-entry component will contain lots of values which I would like to be propagated back to the top level list of objects:
Vue.component("shortcut-entry", {
props: {
mod_ctrl: Boolean,
mod_alt: Boolean,
mod_shift: Boolean,
keypress: String,
action: String,
...
},
Each of those properties exists as a separate checkbox / input on the page with (for example) <input ... v-model="action">. The way I understand it, I could wire the update events back to the parent component and do replacements there... but that sounds like a lot of boilerplate code.
Can I somehow propagate any modifications for those props back to the parent component automatically? (avoiding the "Avoid mutating a prop directly" warning)
It seems to work as I expect if I move every prop I currently have into another level (so I have props: {options: Object}, v-bind it with .sync and assign everything there), but I'm looking into some more explicit solution which actually declares the relevant options ahead of time.
You can use sync modifier with the props object notation together. Instead of v-bind="shortcut" use v-bind.sync="shortcut"
This way shortcut component can declare all props (instead of just options object) and still be able to notify parent about mutation
But you need to change how you bind your inputs. Instead of <input ... v-model="action"> you need to <input ... v-bind:value="action" v-on:input="$emit('update:action', $event.target.value)">. Problem with this is that different <input> types use different names for value and change event
To work around it and keep using v-model you can declare computed for each prop and use v-model="actionComputed"
computed: {
actionComputed: {
get: function() { return this.action },
set: function(value) { this.$emit('update:action', value) }
}
}
So the result is again lot of boilerplate...
TL:DR
If you want all props declared ahead of time (instead of single prop of type object), some boilerplate is necessary. It can be reduced by generating computed props for v-model on the fly (same way Vuex helpers generate computed to access and update Vuex state) but I don't think it is worth the effort. Just pass an object and let the <input> components mutate it's properties...

Vue - Access child component default props after mounted

I'm trying to figure out what the default properties (props) were for a child component. In this example, I have two components A and B. B wraps around A and A is passed properties. I'd like to figure out what the default values were for the component A from the component B which wraps around it, most importantly the types specified for the defaults. To be clear, in the mounted function of B I'd like to be able to see the default values and types of A assuming B will always have exactly 1 child component.
I've tried getting the child component using this.$children[0]._props in the mounted lifecycle hook, but those properties are the ones set. I've tried getting the properties earlier in the Vue lifecycle (like created, beforeCreate, beforeMount etc.) except they don't seem to exist until mounting. I've also inspected the this.$children[0] object using the browser console, and haven't found any default values for the props (only getter functions which retrieve the override defaults). I'm willing to pass extra data to B in order to get the default properties, but would prefer not to (since it seems redundant, i.e. I should know what the component "origin" was by looking at the this.$children[0] object).
Minimal example is located here: https://codepen.io/maxschommer/pen/wvaqGjx
I've included the HTML and JS below for quick reference.
JS:
Vue.component('A', {
name: "A",
props: {
prop1: {
type: String,
default: "The First Default Value"
},
prop2: {
type: String,
default: 'The Second Default Value'
}
},
template: `<div class="A">
<h1>A Prop 1: {{ prop1 }} </h1>
<h1>A Prop 2: {{ prop2 }} </h1>
</div>`
});
Vue.component('B', {
name: "B",
mounted: function() {
this.$children[0];
// I'd like to find some way to get the default properties
// of the child of this component (A) here (or in computed, etc.). Instead
// this gives the values which were set.
alert(JSON.stringify(this.$children[0]._props));
},
template:
`<div><slot></slot></div>`});
var parent = new Vue({
el: "#app",
template:
`<div class=templateField>
<B>
<A prop1="Overriding" prop2="Defaults"></A>
</B>
</div>`
});
HTML:
<div id="app">
</div>
PS: I'm a bit confused about the difference between components and elements when refering to Vue (I believe components are JS objects and elements are when they are rendered to html) so please correct my terminology if I'm getting it wrong.
You can access the original options object (the object you give Vue to construct component instances) from this.$options, so
mounted() {
const propDfns = this.$options.__proto__.props
const propTypes = Object.values(propDfns).map(p => p.type.name)
console.log(propTypes)
},
Components are not only JS objects. they are mixture of js, html or template and css

Why is this Vue prop not reacting to change?

I have a Prop in my component that is a User object, I then have this function:
onChange: function(event){
this.$v.$touch();
if (!this.$v.$invalid) {
this.$axios.put('update',{
code:this.user.code,
col:event.target.name,
val:event.target.value
}).then(response => {
this.user[event.target.name]=event.target.value
});
}
}
I can see in the Vue console debugger that the correct attribute has been updated, this attribute exists when the component is created but the template where I reference it does not refresh:
<p>Hi {{user.nickname}}, you can edit your details here:</p>
This is all within the same component so I'm not navigating to child or parent. I'm sure props have been reactive elsewhere in my code?
Ok, it seems this is intended behaviour. According to the documentation
https://v2.vuejs.org/v2/guide/components-props.html in the scenario that I have it should be handled as:
The prop is used to pass in an initial value; the child component wants to use it as a local data property afterwards. In this case,
it’s best to define a local data property that uses the prop as its
initial value:
props: ['initialCounter'],
data: function () {
return {
counter: this.initialCounter
}
}
Usually components should be reactive to Props, though i have had experiences where it was non-reactive so i added the prop to a watcher and put the functional call there.
props: ["myProp"],
watch:{
myProp(){
// ... your functions here
}
}

VueJS right way to edit prop without changing parent data

In my parent vue component I have a user object.
If I pass that user object to a child component as a prop:
<child :user="user"></child>
and in my child component I update user.name, it will get updated in the parent as well.
I want to edit the user object in child component without the changes being reflected in the user object that is in parent component.
Is there a better way to achieve this than cloning the object with: JSON.parse(JSON.stringify(obj))?
You don't have to use the JSON object.
const child = {
props:["user"],
data(){
return {
localUser: Object.assign({}, this.user)
}
}
}
Use localUser (or whatever you want to call it) inside your child.
Edit
I had modified a fiddle created for another answer to this question to demonstrate the above concept and #user3743266 asked
I'm coming to grips with this myself, and I'm finding this very
useful. Your example works well. In the child, you've created an
element in data that takes a copy of the prop, and the child works
with the copy. Interesting and useful, but... it's not clear to me
when the local copy gets updated if something else modifies the
parent. I modified your fiddle, removing the v-ifs so everything is
visible, and duplicating the edit component. If you modify name in one
component, the other is orphaned and gets no changes?
The current component looks like this:
Vue.component('edit-user', {
template: `
<div>
<input type="text" v-model="localUser.name">
<button #click="$emit('save', localUser)">Save</button>
<button #click="$emit('cancel')">Cancel</button>
</div>
`,
props: ['user'],
data() {
return {
localUser: Object.assign({}, this.user)
}
}
})
Because I made the design decision to use a local copy of the user, #user3743266 is correct, the component is not automatically updated. The property user is updated, but localUser is not. In this case, if you wanted to automatically update local data whenever the property changed, you would need a watcher.
Vue.component('edit-user', {
template: `
<div>
<input type="text" v-model="localUser.name">
<button #click="$emit('save', localUser)">Save</button>
<button #click="$emit('cancel')">Cancel</button>
</div>
`,
props: ['user'],
data() {
return {
localUser: Object.assign({}, this.user)
}
},
watch:{
user(newUser){
this.localUser = Object.assign({}, newUser)
}
}
})
Here is the updated fiddle.
This allows you full control over when/if the local data is updated or emitted. For example, you might want to check a condition before updating the local state.
watch:{
user(newUser){
if (condition)
this.localUser = Object.assign({}, newUser)
}
}
As I said elsewhere, there are times when you might want to take advantage of objects properties being mutable, but there are also times like this where you might want more control.
in the above solutions, the watcher won't trigger at first binding, only at prop change. To solve this, use immediate=true
watch: {
test: {
immediate: true,
handler(newVal, oldVal) {
console.log(newVal, oldVal)
},
},
}
you can have a data variable just with the information you want to be locally editable and load the value in the created method
data() {
return { localUserData: {name: '', (...)}
}
(...)
created() {
this.localUserData.name = this.user.name;
}
This way you keep it clear of which data you are editing. Depending on the need, you may want to have a watcher to update the localData in case the user prop changes.
According to this, children "can't" and "shouldn't" modify the data of their parents. But here you can see that if a parent passes some reactive data as a property to a child, it's pass-by-reference and the parent sees the child's changes. This is probably what you want most of the time, no? You're only modifying the data the parent has explicitly shared. If you want the child to have an independent copy of user, you could maybe do this with JSON.parse(JSON.stringify()) but beware you'll be serializing Vue-injected properties. When would you do it? Remember props are reactive, so the parent could send down a new user at any time, wiping out local changes?
Perhaps you need to tell us more about why you want the child to have it's own copy? What is the child going to do with its copy? If the child's user is derived from the parent user in some systematic way (uppercasing all text or something), have a look at computed properties.

Categories