I am trying to get my head wrap around how props are passed to child components and how they are updated from there.
// parent.vue
<template>
<div>
<div v-for="elem in getCurrentArray">
<child-component :elem="elem"></child-component>
</div>
<button #click.prevent="currentIdx--">Prev</button>
<button #click.prevent="currentIdx++">Next</button>
</div>
</template>
<script>
export default {
components : {
ChildComponent
},
data(){
return {
arr : [ ["A", "B", "C" ], [ "D", "E" ]], // this would be populated from vue store
currentIdx : 0
}
},
computed : {
getCurrentArray(){
return this.arr[this.currentIdx]
}
},
}
</script>
// child.vue
<template>
<div>Prop: {{elem}} <input type="text" v-model="myinput" #blur="save" /></div>
</template>
<script>
export default {
props : [ "elem" ],
data(){
return {
myinput : this.elem
}
},
methods : {
save(){
// this.$store.dispatch("saveChildElement", { myinput }
}
},
mounted(){ console.log( this.elem + " being rendered" ) }
}
</script>
In this example, I have two sets of arrays ['A','B','C'] and ['D','E']. On page load, the first set is rendered thru child components.
Looks good. However, when I click next to go to the second set, I get this:
So, while the props are being passed correctly, the textbox input values are not updated. When I checked the console.log, it's clear that vue does not re-render child components for "D" and "E". Instead, it simply uses the existing components for "A" and "B".
Is there a way to force vue to re-render the components? If not, how can I make sure that the textbox input gets the latest prop values? Keep in mind that I want to be able to save changes to the input values via vue store.
Add a key as cool-man says, it will fix the issue.
However, your check for component re-rendering is flawed, you used the child mounted life cycle, and because you see it running only once you think the component isn't re-rendering. But this is wrong. "mounted" happens only once in a component life cycle, and because you're using the same component for ['A','B','C'] and then for ['D','E'] vue knows not to re-create the components and simply re-renders them with the new props.
Try to add the :Key prop in the v-for loop
<div v-for="elem in getCurrentArray" :key="elem">
To give Vue a hint so that it can track each node’s identity, and thus reuse and reorder existing elements, you need to provide a unique key attribute for each item:
... List Rendering - Maintaining State
Keys must be unique, if you have two or more same value in your array, for example ['A',' B', 'A'] it will create a conflict, the best way to overcome this is to add a generated unique 'ID' for each instance.
Or In alternative (a short term solution)
You can use the loop index in combination with the array value, this should give you a more or less unique key.
<div v-for="(elem, index) in getCurrentArray" :key="elem + index">
You can ask your component to re-render. (The other answers of updating the key is correct).
However, if you do want to use Vuex to manage state, I suggest you do that first as there is (essentially) a different mechanism that will do that re-render for you, and feel much easier. Namely the “computed properties” will likely solve that for you.
I remember getting stuck on this when I was starting out, and once I implemented Vuex I wished I had just done it that way, which kind of just solved the problem for me as part of its workflow. (It will likely mean you don’t need to update key, but you have that in case you have a complex situation when you still need to force a re-render).
Edit:
As #JonyB rightly pointed out, you are trying to re-render the whole component. If you think about it, what you really want to do here is not re-render the whole component, but merely update the state. Therefore, once you implement Vuex, it will likely solve this for you as it allows you to deal with state separately.
All you need to add the key to the v-for loop and it will work.
Replace
<div v-for="elem in getCurrentArray">
with
<div v-for="elem in getCurrentArray" :key="elem">
Ref: https://v2.vuejs.org/v2/guide/list.html#v-for-with-a-Component
Related
I have been working on a Vue 2 project for a while, and upon upgrading our linting requirements I discovered that we had prop mutation errors in many of our child components. In our project, we pass a singleton object as a prop to many components and were originally updating the object directly from the child components. Vue seems to suggest using the v-bind.sync feature for updating props from child components (or using the equivalent v-bind and v-on). This, however, doesn't solve the issue of prop modification from nested components in an array.
Take this (pseudo)code for example that uses prop mutation:
Note: Assume const sharedObject: { arrayElements: Array<{ isSelected: boolean }> } = ...
Page.vue
<template>
...
<Component1 :input1="sharedObject" />
...
</template>
Component1.vue
<template>
...
<template v-for="elem in sharedObject.arrayElements">
<Component2 :input2="elem" />
</template>
...
</template>
Component2.vue
<template>
...
<q-btn #click="input2.isSelected = !input2.isSelected"></q-btn>
...
</template>
What is the proper way of updating a property like input2.isSelected from nested components in Vue 2? All of the approaches I've thought of are flawed.
Flawed Approaches
I believe that we would like to bubble up that input2.isSelected has been modified in Component2 to Page.vue, however, this seems to either lead to messy code or an uneasy feeling that we are just suppressing linting errors in a roundabout way.
To demonstrate the "messy code" approach, first note that Page.vue does not know the index of of elem in sharedObject.arrayElements. Therefore, we would need to emit an object to Page.vue from Component1 which contains the state of input2.isSelected as well of the index of elem in sharedObject.arrayElements. This gets messy quickly. What about the example where we have:
Component1.vue
<template>
...
<template v-for="elem in sharedObject.arrayElements">
<template v-for="elem2 in elem.arrayElements">
<Component2 :input2="elem2" />
</template>
</template>
...
</template>
in this case, then we could need to pass up 2 indices! It doesn't seem like a sustainable solution to me.
The alternative that I thought of is a callback function (passed as a prop through the component hierarchy) that takes as input the element we want to update and an object that contains the properties we want to update (using Object.assign).
This makes me very uneasy since I don't know the real reason why we can't update a pass-by-reference prop from a child component. To me, it seems like it's just a roundabout way of updating the passed-in from Component2 without the linter noticing. If there is some magic modification that happens to props when they're passed to child components, then surely passing in the object that I received in Component2 to the callback function and modifying it in the parent component would basically just be updating the prop in the child component, but more complicated.
What is the proper way of approaching this problem in Vue 2?
Very good question and analysis of the current state of this long-standing issue in Vue ecosystem.
Yes, modifying "value type" props from the child is a problem as it creates runtime issues (parent overwriting the changes when re-rendered) and thus Vue generates a runtime error when this happens...
Modifying a property of object passed as prop is OK from the "code works fine" POV. Unfortunately there are some influential people in the community with opinion (and many who blindly follow them) that this is an anti-pattern. I do not agree with that and raised my arguments many times (for example here). You described the reasons very well - it just creates unnecessary complexity/boilerplate code...
So what you are dealing with is really just a linting rule (vue/no-mutating-props). There is an ongoing issue/discussion that proposes the configuration option that should allow to ease the strictness of the rule with many good arguments but it gets very little attention from the maintainers (feel free to raise your voice there too)
For now what you can do is:
Disable the rule (far from perfect but luckily thanks to a Vue runtime error you can catch the real incorrect cases during development just fine)
Accept the reality and use workarounds
Workaround - use global state (store like Vuex or Pinia)
Note: Pinia is preferred as next version of Vuex will have same API
General idea is to place the sharedObject in the store and use props only to navigate the child components to the right object - in your case the Component2 will receive an index by prop and retrieve the right element from the store using it.
Stores are great for sharing the global state but using is just to overcome the linting rule is bad. Also as a result, components are coupled to the store hence both reusability suffers and testing is harder
Workaround - events
Yes it is possible to create mess and lot of boilerplate code using only events (especially if you nest components more than 2 levels) but there are ways to make things cleaner.
For example in your case Component2 does not need to know the index as you can handle the event like this
// Component1
<template>
...
<template v-for="elem in sharedObject.arrayElements">
<template v-for="(elem2, index) in elem.arrayElements">
<Component2 :input2="elem2" #update="updateElement($event, index)" />
</template>
</template>
...
</template>
In your case, the Component2 handles only change of single boolean property so $event can be simple boolean. If there are more than one property to be changed inside Component2, $event can be an object and you can use object spread syntax to "simplify" the Component2 (using one event instead of multiple - one for each property)
// Component2
<template>
...
<input v-model="someText" type="text">
<q-btn #click="updateInput('isSelected', !input2.isSelected)"></q-btn>
...
</template>
<script>
export default {
props: ['input2'],
computed: {
someText: {
get() { return this.input2.someText },
set(newVal) { updateInput('someText', newVal) }
}
},
methods: {
updateInput(propName, newValue) {
const updated = { ...this.input2 } // make a copy of input2 object
updated[propName] = newValue // change selected property
this.$emit('update', updated) // send updated object to parent
}
}
}
</script>
Well...I prefer just to disable the rule and set some clear naming conventions to indicate that the component is responsible for changing it's input...
Note that there are other workarounds like using this.$parent, inject\provide or event bus but those are really bad
I'm having trouble understanding the two-way binding between a parent and child component. So far, what I've read seems to recommend using prop.sync in conjunction with watching the prop. E.g:
Parent:
<child-component :childProp.sync="parentData"></child-component>
Child:
<template>
<input v-model="childData" #input="$emit('update:childProp', childData);"></input>
</template>
<script>
export default {
props: ['childProp'],
data() {
childData: childProp
},
watch: {
childProp(newValue) {
this.childData = newValue;
}
}
}
</script>
My problem with this is, wouldn't this create some sort of redundancy when either the parentData or the childData is updated?
The flow would be thus (for parentData changed):
parentData changes ->
triggers watch ->
childData changes ->
triggers .sync ->
parentData updated.
I'm assuming the cycle stops at step #5 because the updated value of the parentData is the same as the old value, so parentData doesn't really change, hence step #2 watcher is not triggered.
My problem with this is, if my reasoning is right, there will be some sort of redundancy in that changes to the parentData will go to the child and reflect back to itself, and vice versa. The reflection is the redundancy. Am I right so far? Or is my understanding of this completely off?
If I'm wrong, please help me understand where I went wrong. But if I'm right, then is there another way to achieve two-way binding without this redundancy?
I suppose you can simplify a child component code like this:
<template>
<input :value="childProp" #input="$emit('update:childProp', $event);"></input>
</template>
<script>
export default {
props: ['childProp']
}
</script>
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...
I have a component who initialized like this
<custom :opts="{map: false}"></custom>
and there is HTML similar to this
<template id="custom">
<div v-if="opts.map">
I'm awesome
</div>
<button v-on:click="show"></button>
</template>
where
function show(){
this.opts = {map:true} // (1) <-- This is working and I could see hidden div
this.opts.map = true // (2) <-- For some reason not working
Vue.set(this.opts, 'map', true) // (3) <-- Still not working
}
So my question is why variant 2 doesn't work and what should I change to make my control react to value reset on a button click. Or a proper explanation why (1) is working, but (2) isn't - also will be accepted as an answer.
The real problem with the code (all 3 versions) is changing a component's property from within a component. In idiomatic Vue, only the parent should change properties. If a component needs to effect a change, it should emit an event to the parent and let the parent make the necessary changes. Otherwise, there is ambiguity in which component "owns" the property.
One Way Data Flow
All props form a one-way-down binding between the child property and the parent one: when the parent property updates, it will flow down to the child, but not the other way around.
Sending Messages to Parents with Events
Can be off base here but I believe this happens because in vue component props are not reactive, so their objects are not being observed in depth. Or rather they are "a little bit reactive", reassigning the root prop does cause the DOM update but is not expected to be done manually and you'll see a warning when doing such on development build:
[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: "..."
And for as why props are not completely reactive in the first place: https://v2.vuejs.org/v2/guide/components-props.html#One-Way-Data-Flow
To work around the whole issue you must pass any necessary props to the component data and if those props were passed as nested objects you might also want to completely avoid mutating them from within the component since it will propagate to the parent which, unless clearly mentioned, can be a source of bad news.
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.