Deeply nested data objects in VueJS - vue.js

I've got a VueJs front end that fetches some data from an API. The app uses vue-router.
The data fetched for one component is similar to the following:
{
name: ...,
email: ...,
order: {
data: {
line_items: [
{
quantity: ...
}
]
}
}
}
The component is instantiated with a data object called info:
data () {
return {
info: {}
}
}
In the beforeRouteEnter hook, the data is fetched by a vue-resource http.get and info is set to the body of the result like this:
vm.info = result.body
When the component renders, the following errors are produced:
TypeError: undefined is not an object (evaluating _vm.order.data.line_items')
In the template, the data is referenced in curly braces as per usual, however, if I just reference info in the template like this:
{{ info }}
it will output all of the data and not complain at all.
What is the correct way to assign a deeply nested data object?

If you are finding #saurabh answer is not working then you may need to check how you are assigning the new values to your object.
Firstly is the data being accidiently set as a string? hence {{ info }} working (or appearing to). May be worth using response.json() to set the data.
If thats not it then the error may be produced as the data you have set is not reactive. As you are assigning a nested object you may need to use different methods to make it reactive, i.e
Vue.set(vm.someObject, 'b', 2)
or
this.someObject = Object.assign({}, this.someObject, { a: 1, b: 2 })
check out: https://v2.vuejs.org/v2/guide/reactivity.html#Change-Detection-Caveats
because your response is an object you may want to break out your data into corresponding params, i.e.
data () {
return {
info: {
name: '',
email: '',
order: {},
},
}
}
then you can assign name & email as you expected (info.email = ...).
For info.order you'd use Vue.set:
Vue.set(this.info, 'order', result.body.order)

The actual issue here is a life cycle one. The route guard beforeRouteEnter is called after the component is created so the error is thrown because the data isn’t there when the component tries to access it.

You have to use condition rendering here, which you can easily do with help of Vue directive v-if. It may give error if the data is not populated and you try to access it, so v-if will render that part of HTML only when data is present.
You need to do something like following:
<div v-if="info.order">
<span>
{{ info.order }}
</span>
</div>

In my scenario, I had to use one Vue.set that wrapped an Object.assign:
I'm trying to set state.workouts[state.workoutDate].isDone in a Vuex mutation
toggleWorkout(state, isDone) {
Vue.set(
state.workouts,
state.workoutDate,
Object.assign({}, state.workouts[state.workoutDate], { isDone: isDone })
);
},

Force update object setting a new object with same content
<button #click="my.object.nested.variable = 2; my = Object.assign({}, my);">
or
this.my.object.nested.variable = 2;
this.my = Object.assign({}, this.my);

Related

Vue.js 2 - For-Loop: Property or method "item" is not defined on the instance but referenced during render

I have a Component which loads the needed data via an API-Call. I save the response object into a data property
data() {
return {
productData: {},
hasError: false,
errorMessage: '',
};
},
The save method looks like that:
if (response.data.status === 'error') {
this.hasError = true;
this.errorMessage = response.data.errorMessage;
} else {
this.productData = JSON.parse(response.data.data);
}
Insight the data are some properties like name, ean and other and additionally an array of Objects. Now I write the Information of the productData to the template without any pain.
Than I try to iterate over the nested array of the product and I want to render a different component to encapsulate this behavior.
<supplier-item v-for="item in productData.supplierItems" :key="item.id"></supplier-item>
And that's the point where the pain comes in:
[Vue warn]: Property or method "item" is not defined on the instance but referenced during render. Make sure that this property is reactive, either in the data option, or for class-based components, by initializing the property.
The given link says nothing to me.
I can't understand the logic. How can an item - which is initialized insight the for loop - not be referenced during render.
Maybe someone can answer my question. I had this error as more as any other.
Thank you!
You can check with v-if if supplierItems exists like:
<div v-if="productData && productData.supplierItems">
<supplier-item v-for="item in productData.supplierItems" :key="item.id" />
</div>

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.

Vuetify Centralize Rules [duplicate]

The following code has been written to handle an event after a button click
var MainTable = Vue.extend({
template: "<ul>" +
"<li v-for='(set,index) in settings'>" +
"{{index}}) " +
"{{set.title}}" +
"<button #click='changeSetting(index)'> Info </button>" +
"</li>" +
"</ul>",
data: function() {
return data;
}
});
Vue.component("main-table", MainTable);
data.settingsSelected = {};
var app = new Vue({
el: "#settings",
data: data,
methods: {
changeSetting: function(index) {
data.settingsSelected = data.settings[index];
}
}
});
But the following error occurred:
[Vue warn]: Property or method "changeSetting" is not defined on the instance but referenced during render. Make sure to declare reactive data properties in the data option. (found in <MainTable>)
Problem
[Vue warn]: Property or method "changeSetting" is not defined on the instance but referenced during render. Make sure to declare reactive data properties in the data option. (found in <MainTable>)
The error is occurring because the changeSetting method is being referenced in the MainTable component here:
"<button #click='changeSetting(index)'> Info </button>" +
However the changeSetting method is not defined in the MainTable component. It is being defined in the root component here:
var app = new Vue({
el: "#settings",
data: data,
methods: {
changeSetting: function(index) {
data.settingsSelected = data.settings[index];
}
}
});
What needs to be remembered is that properties and methods can only be referenced in the scope where they are defined.
Everything in the parent template is compiled in parent scope; everything in the child template is compiled in child scope.
You can read more about component compilation scope in Vue's documentation.
What can I do about it?
So far there has been a lot of talk about defining things in the correct scope so the fix is just to move the changeSetting definition into the MainTable component?
It seems that simple but here's what I recommend.
You'd probably want your MainTable component to be a dumb/presentational component. (Here is something to read if you don't know what it is but a tl;dr is that the component is just responsible for rendering something – no logic). The smart/container element is responsible for the logic – in the example given in your question the root component would be the smart/container component. With this architecture you can use Vue's parent-child communication methods for the components to interact. You pass down the data for MainTable via props and emit user actions from MainTable to its parent via events. It might look something like this:
Vue.component('main-table', {
template: "<ul>" +
"<li v-for='(set, index) in settings'>" +
"{{index}}) " +
"{{set.title}}" +
"<button #click='changeSetting(index)'> Info </button>" +
"</li>" +
"</ul>",
props: ['settings'],
methods: {
changeSetting(value) {
this.$emit('change', value);
},
},
});
var app = new Vue({
el: '#settings',
template: '<main-table :settings="data.settings" #change="changeSetting"></main-table>',
data: data,
methods: {
changeSetting(value) {
// Handle changeSetting
},
},
}),
The above should be enough to give you a good idea of what to do and kickstart resolving your issue.
Should anybody land with the same silly problem I had, make sure your component has the 'data' property spelled correctly. (eg. data, and not date)
<template>
<span>{{name}}</span>
</template>
<script>
export default {
name: "MyComponent",
data() {
return {
name: ""
};
}
</script>
In my case the reason was, I only forgot the closing
</script>
tag.
But that caused the same error message.
If you're experiencing this problem, check to make sure you don't have
methods: {
...
}
or
computed: {
...
}
declared twice
It's probably caused by spelling error
I got a typo at script closing tag
</sscript>
Remember to return the property
Another reason of seeing the Property "search" was accessed during render but is not defined on instance is when you forget to return the variable in the setup(){} function
So remember to add the return statement at the end:
export default {
setup(){
const search = ref('')
//Whatever code
return {search}
}
}
Note: I'm using the Composition API
Adding my bit as well, should anybody struggle like me, notice that methods is a case-sensitive word:
<template>
<span>{{name}}</span>
</template>
<script>
export default {
name: "MyComponent",
Methods: {
name() {return '';}
}
</script>
'Methods' should be 'methods'
If you use two times vue instance. Then it will give you this error. For example in app.js and your own script tag in view file. Just use one time
const app = new Vue({
el: '#app',
});
I got this error when I tried assigning a component property to a state property during instantiation
export default {
props: ['value1'],
data() {
return {
value2: this.value1 // throws the error
}
},
created(){
this.value2 = this.value1 // safe
}
}
My issue was I was placing the methods inside my data object. just format it like this and it'll work nicely.
<script>
module.exports = {
data: () => {
return {
name: ""
}
},
methods: {
myFunc() {
// code
}
}
}
</script>
In my case, I wrote it as "method" instead of "methods". So stupid. Wasted around 1 hour.
Some common cases of this error
Make sure your component has the data property spelled correctly
Make sure your template is bot defined within another component’s template.
Make sure you defined the variable inside data object
Make sure your router name in string
Get some more sollution
It is most likely a spelling error of reserved vuejs variables. I got here because I misspelled computed: and vuejs would not recognize my computed property variables. So if you have an error like this, check your spelling first!
I had two methods: in the <script>, goes to show, that you can spend hours looking for something that was such a simple mistake.
if you have any props or imported variables (from external .js file) make sure to set them properly using created like this;
make sure to init those vars:
import { var1, var2} from './constants'
//or
export default {
data(){
return {
var1: 0,
var2: 0,
var3: 0,
},
},
props: ['var3'],
created(){
this.var1 = var1;
this.var2 = var2;
this.var3 = var3;
}
In my case it was a property that gave me the error, the correct writing and still gave me the error in the console. I searched so much and nothing worked for me, until I gave him Ctrl + F5 and Voilá! error was removed. :'v
Look twice the warning : Property _____ was accessed during render but is not defined on instance.
So you have to define it ... in the data function for example which commonly instantiate variables in a Vuejs app. and, it was my case and that way the problem has been fixed.
That's all folk's !
In my case, I forgot to add the return keyword:
computed: {
image(){
this.productVariants[this.selectedVariant].image;
},
inStock(){
this.productVariants[this.selectedVariant].quantity;
}
}
Change to:
computed: {
image(){
return this.productVariants[this.selectedVariant].image;
},
inStock(){
return this.productVariants[this.selectedVariant].quantity;
}
}
In my case due to router name not in string:
:to="{name: route-name, params: {id:data.id}}"
change to router name in string:
:to="{name: 'router-name', params: {id:data.id}}"
In my case I was trying to pass a hard coded text value to another component with:
ChildComponent(:displayMode="formMode")
when it should be:
ChildComponent(:displayMode="'formMode'")
note the single quotes to indicate text instead of calling a local var inside the component.
If you're using the Vue3 <script setup> style, make sure you've actually specified setup in the opening script tag:
<script setup>
I had lapsed into old habits and only created a block with <script>, but it took a while to notice it.
https://v3.vuejs.org/api/sfc-script-setup.html
Although some answers here maybe great, none helped my case (which is very similar to OP's error message).
This error needed fixing because even though my components rendered with their data (pulled from API), when deployed to firebase hosting, it did not render some of my components (the components that rely on data).
To fix it (and given you followed the suggestions in the accepted answer), in the Parent component (the ones pulling data and passing to child component), I did:
// pulled data in this life cycle hook, saving it to my store
created() {
FetchData.getProfile()
.then(myProfile => {
const mp = myProfile.data;
console.log(mp)
this.$store.dispatch('dispatchMyProfile', mp)
this.propsToPass = mp;
})
.catch(error => {
console.log('There was an error:', error.response)
})
}
// called my store here
computed: {
menu() {
return this.$store.state['myProfile'].profile
}
},
// then in my template, I pass this "menu" method in child component
<LeftPanel :data="menu" />
This cleared that error away. I deployed it again to firebase hosting, and voila!
Hope this bit helps you.
It seems there are many scenarios that can trigger this error. Here's another one which I just resolved.
I had the variable actionRequiredCount declared in the data section, but I failed to capitalize the C in Count when passing the variable as a params to a component.
Here the variable is correct:
data: () => {
return{
actionRequiredCount: ''
}
}
In my template it was incorrect (notd the no caps c in "count"):
<MyCustomModule :actionRequiredCount="actionRequiredcount"/>
Hope this helps someone.
Most people do have an error here because of:
a typo or something that they forgot to declare/use
the opposite, did it in several places
To avoid the typo issues, I recommend always using Vue VSCode Snippets so that you don't write anything by hand by rather use vbase, vdata, vmethod and get those parts generated for you.
Here are the ones for Vue3.
You can of course also create your own snippets by doing the following.
Also make sure that you're properly writing all the correct names as shown here, here is a list:
data
props
computed
methods
watch
emits
expose
As for the second part, I usually recommend either searching the given keyword in your codebase. So like cmd + f + changeSetting in OP's case to see if it's missing a declaration somewhere in data, methods or alike.
Or even better, use an ESlint configuration so that you will be warned in case you have any kind of issues in your codebase.
Here is how to achieve such setup with a Nuxt project + ESlint + Prettier for the most efficient way to prevent bad practices while still getting a fast formatting!
One other common scenario is:
You have a component (child) extending another component (parent)
You have a property or a method xyz defined under methods or computed on the parent component.
Your are trying to use parent's xyz, but your child component defines its own methods or computed
Sample code with the problem
// PARENT COMPONENT
export default {
computed() {
abc() {},
xyz() {} // <= needs to be used in child component
},
...
}
// CHILD COMPONENT
export default {
extends: myParentComponent,
computed() {
childProprty1() {},
childProprty2() {}
}
}
The solution
In this case you will need to redefine your xyz computed property under computed
Solution 1:
Redefine xyz and copy the code from the parent component
// CHILD COMPONENT
export default {
extends: myParentComponent,
computed() {
xyz() {
// do something cool!
},
childProprty1() {},
childProprty2() {}
}
}
Solution 2
Redefine xyz property reusing parent component code (no code redundancy)
// CHILD COMPONENT
export default {
extends: myParentComponent,
computed() {
xyz() {
return this.$parent.$options.computed.xyz
},
childProprty1() {},
childProprty2() {}
}
}
For me it happened because I wrote method: instead of methods: (plural). It's a silly mistake but it can happen :)
In my case it was the methods: { } I had put the } before my method functions so for example I had it like this methods: { function , function }, function, function so some of the functions that were out of the curly braces were not included inside the methods function.

Updates to object inside array do not trigger updates

In my root Vue instance, I have an array of objects with some data, which I use to render a set of components. These components have a watcher on the object of data provided to them, which is supposed to make an asynchronous call every time the object is updated.
The problem is that when I update a property of one of the objects in my array, the watcher is not called. It shouldn't fall into any of Vue's caveats because a) I'm not adding a new property, just updating an existing one and b) I'm not mutating the array itself in any way. So why is this happening? And how do I fix it?
My main Vue instance:
let content = new Vue({
el: '#content',
data: {
testData: [
{ name: 'test1', params: {
testParam: 1
} },
{ name: 'test2', params: {
testParam: 1
} },
{ name: 'test3', params: {
testParam: 1
} }
]
}
});
The code which I use to render my components:
<div id="content">
<div v-for="item in testData">
<test-component v-bind="item"></test-component>
</div>
</div>
And my component:
Vue.component('test-component', {
props: {
name: {
type: String,
required: true
},
params: {
type: Object,
required: true
}
},
data: function() {
return { asyncResult: 0 };
},
watch: {
params: function(newParams, oldParams) {
// I use a custom function to compare objects, but that's not the issue since it isn't even being called.
console.log(newParams);
if(!this.compareObjs(newParams, oldParams)) {
// My async call, which mutates asyncResult
}
}
},
template: `
<span>{{ asyncResult }}</span>
`
});
My goal is to mutate the properties of the params property of a given object and trigger the watcher to rerender the corresponding component, but when I try to mutate it directly it doesn't work.
Example (and the way I'd like my component to work):
content.testData[2].params.testParam = 5;
Unfortunately, it doesn't. Using Vue.set doesn't work either:
Vue.set(content.testData[2].params, 'testParam', 5);
The only thing I found which does work is to assign a new object entirely (which is not something I'd like to do every time I have to mutate a property):
content.testData[2].params = Object.assign({}, content.testData[2].params, { testParam: 5 });
I also tried using a deep watcher, as suggested in a similar question, but it didn't work in my case. When I use the deep watcher the function is called, but both newParams and oldParams are always the same object, no matter which value I set to my property.
Is there a solution to this that will allow me to mutate the array items just by setting a property? That would be the most desirable outcome.
First things first.
Using Vue.set isn't going to help. Vue.set is used to set the values of properties that Vue's reactivity system can't track. That includes updating arrays by index or adding new properties to an object but neither of those apply here. You're updating an existing property of a reactive object, so using Vue.set won't do anything more than setting it using =.
Next...
Vue does not take copies of your objects when passing them as props. If you pass an object as a prop then the child component will get a reference to the same object as the parent. A deep watcher will trigger if you update a property within that object but it's still the same object. The old and new values passed to the watcher will be the same object. This is noted in the documentation:
https://v2.vuejs.org/v2/api/#vm-watch
Note: when mutating (rather than replacing) an Object or an Array, the old value will be the same as new value because they reference the same Object/Array. Vue doesn’t keep a copy of the pre-mutate value.
As you've noticed, one solution is to use a totally new object when performing the update. Ultimately, if you want to compare the old and new objects then you have no choice but to make a copy of the object somewhere. Taking a copy when mutating is a perfectly valid choice, but it's not the only option.
Another option would be to use a computed property to create the copy:
new Vue({
el: '#app',
data () {
return {
params: {
name: 'Lisa',
id: 5,
age: 27
}
}
},
computed: {
watchableParams () {
return {...this.params}
}
},
watch: {
watchableParams (newParams, oldParams) {
console.log(newParams, oldParams)
}
}
})
<script src="https://unpkg.com/vue#2.6.10/dist/vue.js"></script>
<div id="app">
<input v-model="params.name">
<input v-model="params.id">
<input v-model="params.age">
</div>
A few notes on this:
The computed property in this example is only creating a shallow copy. If you needed a deep copy it would be more complicated, something like JSON.stringify/JSON.parse might be an option.
The computed property doesn't actually have to copy everything. If you only want to watch a subset of the properties then only copy those.
The watch doesn't need to be deep. The computed property will create dependencies on the properties it uses and if any of them changes it will be recomputed, creating a new object each time. We just need to watch that object.
Vue caches the values of computed properties. When a dependency changes the old value is marked as stale but it isn't immediately discarded, so that it can be passed to watchers.
The key advantage of this approach is where the copying is handled. The code doing the mutating doesn't need to worry about it, the copying is performed by the same component that needs the copy.
As you said, you will need to use deep property in watch.
Using Vue.set you should remounting the entire object inside your array, like:
const newObj = {
name: 'test1',
params: {
testParam: 1,
},
};
Vue.set(yourArray, newObj, yourIndex);
Note you are setting some value inside your array and in this case the array contains objects.

How to automatically construct watch property based on data attributes in Vue.js?

I have standard Vue.js component and I'd like to convert attributes in data property to watcher or in other words I want to construct a watch object based on data property attributes automatically
my idea looks something like this
watch: {
...(() => {
const watchers = {}
Object.keys(this.$data).forEach(key => {
watchers[key] = () => {
new ProductNutrientUpdate(this).run()
}
})
return watchers
})(),
},
the problem with this approach is that this.$data is not constructed yet
maybe there is some way how I can add watchers in created hook for example??
Vue already watches properties of the data object (note if any of these values are themselves objects, I think you need to update the whole object, i.e. change its value to a shallow copy with the desired nested key-values).
Refer to: https://v2.vuejs.org/v2/guide/reactivity.html
You can then use the update lifecycle hook to watch for all changes to data: https://v2.vuejs.org/v2/api/#updated
I was able to resolve a challenge using the following approach
created() {
Object.keys(this.$data).forEach(key => {
this.$watch(key, function() {
// ... some logic to trigger on attribute change
})
})
}