Object's property is undefined when accessing it through computed props - vue.js

I'm getting a problem, when I try to access object's proprety through a compted property, it get's undefined. The object is set async.
Two things to be clarified:
1- when I log the object to the console inside the computed function -> property (state) is set. BUT when I log the property itself, it is undefined.
computed: {
propA() {
console.log(this.booking);
// { state: "Draft", product: (...) }
console.log(this.booking.state);
//undefined
console.log(this.booking.product);
//"Product abc"
}
}
2- this.booking.state is correctly bound to the view via v-model.
booking is set as follows:
props: {
currentBooking: Object,
},
data() {
return {
booking: Object,
};
},
async mounted() {
this.booking = { ...this.currentBooking }; // {product: "Product abc"}
this.booking.state = "Draft";
}

Since the data is set async, the property is undefined at the time it's logged. The only reason you see it when logging the object is thanks to the console's ability to update objects/arrays. It updates itself to show the current value when you click an object to view the properties.
This only works for references (like objects / arrays) because the console uses that reference to update itself.
So when you log the non-object property, you see the data as it was at the time of the log: undefined. But when you log the object, and then click to expand properties, you see the data as it is now.

Related

vue unexpected reactivity from props

I just noticed an unexpected behaviour and now I don't know if it is normal or not.
I have a component named follows and a child component named follow-list-modal
I'm passing a followList (pagination ) from follows to its child component follow-list-modal
In the follow-list-modal I store the paginated array in the variable members
Follows.vue
<template>
<div>
<follow-list-modal
:follow-list="dataset">
</follow-list-modal>
</div>
</template>
<script>
export default {
props: {
dataset: {
type: Object,
default: {},
},
},
}
</script>
FollowListModal.vue
<template>
<div>
<button #click="fetchMore"> More </button>
</div>
</template>
<script>
export default {
props: {
followList: {
type: Object,
default: {},
},
data() {
return {
members: this.followList.data,
dataset: this.followList,
};
},
methods: {
fetchMore() {
let nextPage = parseInt(this.dataset.current_page) + 1;
axios
.get(this.dataset.path + '?page=' + nextPage)
.then(({ data }) => this.refresh(data))
.catch((error) => console.log(error));
}
},
refresh(paginatedCollection) {
this.dataset = paginatedCollection;
this.members = this.members.concat(...paginatedCollection.data);
},
}
When I click the button More in the follow-list-modal to get more data, I then want to append the new data to the members array.
The unexpected behaviour ( for me at least ). is that if I use push in the refresh method
this.members.push(..paginatedCollection.data);
It appends data not only to members but also to followList which is data that comes from the parent component follows
But if I use concat instead of push, it appends data only to members variable and not to followList
this.members = this.members.concat(..paginatedCollection.data);
Is this behaviour normal ?
I don't get why the followList changes when the members variable changes, I thought that reactivity is one way.
In other words, the members changes when the followList changes, but not the other way around
P.S I don't emit any events from follow-list-modal to follows or change the data of the follows component in any way from the follow-list-modal
In JavaScript, the properties of an Object that are also Objects themselves, are passed by reference, and not by value. Or you might say that they are shallow copied.
Thus, in your example, this.members and this.followList.data are pointing to the same variable in memory.
So, if you mutate this.members, it will mutate this.followList.data as well.
You could avoid this by doing a deep copy of the objects. The easiest method, and arguably the fastest, would be to use JSON.parse(JSON.stringify(obj)), but look at this answer for more examples.
data() {
return {
members: [],
dataset: [],
};
},
created() {
this.members = JSON.parse(JSON.stringify(this.followList.data));
this.dataset = JSON.parse(JSON.stringify(this.followList));
}
You instantiate your data with a direct link to the (initially undefined) property of your prop. This property is a complex entity like an Object (Arrays are Objects), and is thus called via reference. Since members references the same thing in memory as followList.data, when you're calling members, it will follow the reference to the same entity as followList.data. This doesn't have to do with Vue2 reactivity, but here's a link nontheless.
push mutates the array it is called on; it will follow the reference through members and change followList.data, updating its value when called through followList as well. Because the data key is not present on instantiation of the component, Vue can't watch it (just like you need to use Vue.set when adding a new key to a data object).
concat returns a new array of merged elements, and then replaces
the reference in members with the new array. Therefore from this point on you'll
no longer mutate followList.data, even with a push, as the reference has changed to a new entity.
When trying to set your initial members and dataset, I suggest using an initialization method that creates a clone of your followList and writes that to dataset, and running this on the created() or mounted() hook of your component lifecycle. Then create a computed property for members, no need to store followList.data thrice and potentially have dataset and members diverge.

vuex: do not mutate vuex store state outside mutation

I have the following scenario:
Component Textfield:
<v-text-field
v-model="form.profile.mobile_business"
label="Mobile"
prepend-inner-icon="mdi-cellphone"
></v-text-field>
I get the current value via:
data() {
return {
form: {
profile: JSON.parse(JSON.stringify(this.$store.getters["user/Profile"])),
},
};
},
I have a submit button that calls this method:
updateUserProfile() {
this.$store.dispatch("user/updateProfile", this.form.profile);
}
Everything works perfect. On my store dispatch I make the API call and update the store via my mutation:
context.commit('UPDATE_PROFILE', profile);
No errors until this step.
But if I change the form input again - after I pressed the submit button, I get:
vuex: do not mutate vuex store state outside mutation
But I don't want to change the vuex store just when I change the value on my form input.
It should only be updated if someone hits the submit button.
v-model provides 2-way data binding. Changing anything in the view will automatically attempt to update the model directly, rather than through a mutation. Thankfully, Vue allows get and set on computed properties to help us past that.
What you should do on your textfield component is add a computed property with get and set methods. It will look something like this:
computed: {
userProfile: {
get() {
JSON.parse(JSON.stringify(this.$store.getters["user/Profile"]));
},
set() {
// only commit the changes to the form, do not submit the action that calls the API here.
this.$store.commit("user/updateProfile", this.form.profile)
}
}
Your v-model attribute should then be set to this newly created property, and any 'set' operations (read: a user changing the input value) will call the action as opposed to attempting to set the value in the Store directly.
Here is a live example: CodePen
I solved it this way:
form: {
profile: _.cloneDeep(this.$store.getters['user/Profile'])
},
and added a watch handler:
form: {
handler: _.debounce(function (form) {
console.log("watch fired");
}, 500), deep: true
}
so if the user changes the value, nothing happens (except my console.log action).
if he presses the submit button, the store dispatch action will be fired.

Tracking a child state change in Vue.js

I have a component whose purpose is to display a list of items and let the user select one or more of the items.
This component is populated from a backend API and fed by a parent component with props.
However, since the data passed from the prop doesn't have the format I want, I need to transform it and provide a viewmodel with a computed property.
I'm able to render the list and handle selections by using v-on:click, but when I set selected=true the list is not updated to reflect the change in state of the child.
I assume this is because children property changes are not tracked by Vue.js and I probably need to use a watcher or something, but this doesn't seem right. It seems too cumbersome for a trivial operation so I must assume I'm missing something.
Here's the full repro: https://codesandbox.io/s/1q17yo446q
By clicking on Plan 1 or Plan 2 you will see it being selected in the console, but it won't reflect in the rendered list.
Any suggestions?
In your example, vm is a computed property.
If you want it to be reactive, you you have to declare it upfront, empty.
Read more here: reactivity in depth.
Here's your example working.
Alternatively, if your member is coming from parent component, through propsData (i.e.: :member="member"), you want to move the mapper from beforeMount in a watch on member. For example:
propsData: {
member: {
type: Object,
default: null
}
},
data: () => ({ vm: {}}),
watch: {
member: {
handler(m) {
if (!m) { this.vm = {}; } else {
this.vm = {
memberName: m.name,
subscriptions: m.subscriptions.map(s => ({ ...s }))
};
}
},
immediate: true
}
}

UI not updating when nested array property value deleted, only when added

I have a page where an object with nested array values are passed in from the parent component. The user can then, using a series of events and components manage the data in these subscriptions. Currently I'm facing an issue where when a subscriptionId is removed from the props, conditions on the page aren't changing, but they do when it's added.
Child Component
export default {
props: {
// Format of this object is:
// { "gameId": [
// 'subscriptionId',
// 'subscriptionId',
// ] }
subscriptions: {
type: Object,
required: true
}
},
watch: {
subscriptions: {
handler: function (newSubscriptions, oldSubscriptions) {
// NEVER gets fired when `subscriptionId` deleted from array list, but is fired when a new subscription is added
console.log('handler');
}
},
deep: true
}
},
I suspect this might be related to how I'm removing the array from the object. Essentially I'm copying the array, deleting the index in question and overwriting the original array. My hope with this approach is that the watcher wouldn't be needed but it appears to have no impact. Here's the code that exists on the parent component to update the subscriptions:
Parent Component
// Works great, don't have any issues here
handleSubscribed (subscriptionId) {
let newSubscriptions = [subscriptionId];
if (this.subscriptions.hasOwnProperty(this.currentGame.id)) {
newSubscriptions = this.subscriptions[this.currentGame.id];
newSubscriptions.push(subscriptionId);
}
this.$set(this.subscriptions, this.currentGame.id, newSubscriptions);
},
handleUnsubscribed (subscriptionId) {
// if there's more than one, delete only the one we're looking for
if (this.subscriptions.hasOwnProperty(this.currentGame.id) && this.subscriptions[this.currentGame.id].length > 1) {
let newSubscriptions = this.subscriptions[this.currentGame.id];
delete newSubscriptions[newChannels.indexOf(subscriptionId)];
this.$set(this.subscriptions, this.currentGame.id, newSubscriptions);
// shows my subscription has been removed, but GUI doesn't reflect the change
console.log('remove-game', newSubscriptions);
return;
}
this.$delete(this.subscriptions, this.currentGame.id);
},
I was hoping watch might be the solution, but it's not. I've looked over the reactive docs several times and don't see a reason for why this wouldn't work.
VueJS version: 2.5.7
Use Vue.delete instead of the delete keyword.
The object is no longer observable when using delete, therefore not reactive.
Delete a property on an object. If the object is reactive, ensure the deletion triggers view updates. This is primarily used to get around the limitation that Vue cannot detect property deletions, but you should rarely need to use it.

vuejs computed setter of given prop is not reactive

I'm using computed to copy my prop value and use/mutate it in my component:
export default {
props: ['propOffer'],
computed: {
offer: {
get: function () {
return JSON.parse(JSON.stringify(this.propOffer))
},
set: function () {
this.offer
}
},
}
The problem is within using setter. It is not reactive. When I use some kind of input, there is a delay, so my computed offer isn't updating instantly. Example of input:
<v-text-field
label="Offer title"
v-model="offer.title"
></v-text-field>
This is far opposite to the behaviour when I declare offer as a variable (wthout computed) - then I got my {{offer}} changes instantly inside the <template>
How can I improve it? Am I setting my computed wrong?
To better understand this situation, this is what happens at the moment:
When the application loads, the initial state is:
<your-component>
propOffer: '{"title":"test"}'
offer.<lastValue>: undefined
At the point in time, your application will load the v-text-field, this references field offer, and this inits the offer computed variable:
<your-component>
propOffer: '{"title":"test"}'
offer.<lastValue>: [Javascript object 1]
[Javascript object 1]
title: "test"
<v-text-field>
value: "test"
As the user types into the v-text-field, its value changes, because the v-model emits back updates:
<your-component>
propOffer: '{"title":"test"}'
offer.<lastValue>: [Javascript object 1]
[Javascript object 1]
title: "test123"
<v-text-field>
value: "test123"
As you can see here, the setter is never invoked in the normal operation, and hence your code to save it does not run.
You can solve this by making another computed prop for the title of the offer, and then adding some code to prevent your changes from being made undone.
Let's start with the getter & setter for the title:
computed: {
title: {
get() {
return this.offer.title;
},
set(title) {
this.offer = {...this.offer, title};
}
},
// ....
Now we need to properly handle this set operation inside our main offer function, because if we don't handle it, and basically modify its returned object, we get into the territory of undefined behaviour, as the value of the computation doesn't match the computation.
// ...
offer: {
get: function () {
if (this.modifiedOffer) {
return this.modifiedOffer;
}
return JSON.parse(JSON.stringify(this.propOffer))
},
set: function (offer) {
this.modifiedOffer = offer;
}
},
},
data() {
return: {
modifiedOffer: undefined,
};
},
After doing this pattern, you now have a stable application, that shows no undefined behaviour, for more functionality, you basicly need to check if the propOffer changes, and either forcefully delete the this.modifiedOffer, or add more logic to a different computed variable that informs the user there is a data conflict, and ask him to overwrite his data.