Why must vue component data be a function? - vue.js

I am reading up on Vue components, and find their explanation of why data needs to be a function somewhat confusing:
The root instance
var vm = new Vue({
el: '#example',
data: {
message: 'here data is a property'
}
})
A component
var vm = new Vue({
el: '#example',
data: function () {
return {
counter: 0
}
}
})
The Vue docs explain this difference by assigning a global counter variable to each component, and then they act surprised that each component shares that same data... Also they don't explain why they already use a function here.
var data = { counter: 0 }
Vue.component('simple-counter', {
template: '<div>{{ counter }}</div >',
data: function () {
return data
}
})
Of course the data is shared now
<simple-counter></simple-counter>
<simple-counter></simple-counter>
<simple-counter></simple-counter>
When you reference a global object as your data source, it's no surprise that the components don't have their own data. That is also true for root Vue instances that have data as a property.
var mydata = { counter: 0 }
var vm1 = new Vue({
el: '#example1',
data: mydata
})
var vm2 = new Vue({
el: '#example2',
data: mydata
})
So I'm still left with the question why a component can't have a data property?

From my understanding of this, It's to save memory
Many frameworks, such as Angular 2 or, (at times) React, make each instance of a component a separate object. This means that everything each component needs is initialized for every component. Normally though, you really only need to keep a component’s data separate for each initialization. Methods and such stay the same.
Vue avoids that pitfall by having data be a function that returns an object. That allows separate components to have separate internal state without needing to fully re-instantiate the entire component. Methods, computed property definitions, and lifecycle hooks are created and stored only once, and run against every instance of a component.
See this

The data option should always be a function in the context of components which returns a fresh object.
This precaution is made by vue. So whenever you define the object directly in the data option, vue will catch for making the mistake.
Components are never allowed to directly mutate its state. This prevents us from messing up and doing bad things where components do not have their own state.
If this precaution is not made by vue, then you will have a chance to mutate any other components that owns from the component and this would be a security issue.
Example from the documentation:
It’s good to understand why the rules exist though, so let’s cheat.
<div id="example-2">
<simple-counter></simple-counter>
<simple-counter></simple-counter>
<simple-counter></simple-counter>
</div>
var data = { counter: 0 }
Vue.component('simple-counter', {
template: '<button v-on:click="counter += 1">{{ counter }}</button>',
// data is technically a function, so Vue won't
// complain, but we return the same object
// reference for each component instance
data: function () {
return data
}
})
new Vue({
el: '#example-2'
})
Since all three component instances share the same data object, incrementing one counter increments them all! Ouch. Let’s fix this by instead returning a fresh data object:
data: function () {
return {
counter: 0
}
}
Now all our counters each have their own internal state.

It must be a function because otherwhise the data will be shared among all instances of the component, as objects are call by reference rather than call by value. This does not only happen when you reference a global object but also when data is an object itself.
If data is a factory-function that returns an object this object will be created from scratch every time you mount a new instance of the component instead of just passing a reference to the global data.

Because when Vue init data,
function initData(vm){
let data = vm.$options.data
data = vm._data = typeof data === ‘function’ ? getData(data, vm) : data || {}
/*
Because here,data is a reference from vm.$options.data,
if data is an object,
when there are many instances of this Component,
they all use the same `data`
if data is a function, Vue will use method getData( a wrapper to executing data function, adds some error handling)
and return a new object, this object just belongs to current vm you are initializing
*/
……
// observing data
observe(data, true)
}

why Vue forces the data property to be a function is that each instance of a component should have its own data object. If we don’t do that, all instances will be sharing the same object and every time we change something, it will be reflected in all instances.
var vm = new Vue({
el: '#example',
**data: function () {
return {
counter: 0
}**
}

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.

watch props update in a child created programmatically

I created the child using:
const ComponentClass = Vue.extend(someComponent);
const instance = new ComponentClass({
propsData: { prop: this.value }
})
instance.$mount();
this.$refs.container.appendChild(instance.$el);
When this.value is updated in the parent, its value doesn't change in the child. I've tried to watch it but it didn't work.
Update:
There's an easier way to achieve this:
create a <div>
append it to your $refs.container
create a new Vue instance and .$mount() it in the div
set the div instance's data to whatever you want to bind dynamically, getting values from the parent
provide the props to the mounted component from the div's data, through render function
methods: {
addComponent() {
const div = document.createElement("div");
this.$refs.container.appendChild(div);
new Vue({
components: { Test },
data: this.$data,
render: h => h("test", {
props: {
message: this.msg
}
})
}).$mount(div);
}
}
Important note: this in this.$data refers the parent (the component which has the addComponent method), while this inside render refers new Vue()'s instance. So, the chain of reactivity is: parent.$data > new Vue().$data > new Vue().render => Test.props. I had numerous attempts at bypassing the new Vue() step and passing a Test component directly, but haven't found a way yet. I'm pretty sure it's possible, though, although the solution above achieves it in practice, because the <div> in which new Vue() renders gets replaced by its template, which is the Test component. So, in practice, Test is a direct ancestor of $refs.container. But in reality, it passes through an extra instance of Vue, used for binding.
Obviously, if you don't want to add a new child component to the container each time the method is called, you can ditch the div placeholder and simply .$mount(this.$refs.container), but by doing so you will replace the existing child each subsequent time you call the method.
See it working here: https://codesandbox.io/s/nifty-dhawan-9ed2l?file=/src/components/HelloWorld.vue
However, unlike the method below, you can't override data props of the child with values from parent dynamically. But, if you think about it, that's the way data should work, so just use props for whatever you want bound.
Initial answer:
Here's a function I've used over multiple projects, mostly for creating programmatic components for mapbox popups and markers, but also useful for creating components without actually adding them to DOM, for various purposes.
import Vue from "vue";
// import store from "./store";
export function addProgrammaticComponent(parent, component, dataFn, componentOptions) {
const ComponentClass = Vue.extend(component);
const initData = dataFn() || {};
const data = {};
const propsData = {};
const propKeys = Object.keys(ComponentClass.options.props || {});
Object.keys(initData).forEach(key => {
if (propKeys.includes(key)) {
propsData[key] = initData[key];
} else {
data[key] = initData[key];
}
});
const instance = new ComponentClass({
// store,
data,
propsData,
...componentOptions
});
instance.$mount(document.createElement("div"));
const dataSetter = data => {
Object.keys(data).forEach(key => {
instance[key] = data[key];
});
};
const unwatch = parent.$watch(dataFn || {}, dataSetter);
return {
instance,
update: () => dataSetter(dataFn ? dataFn() : {}),
dispose: () => {
unwatch();
instance.$destroy();
}
};
}
componentOptions is to provide any custom (one-off) functionality to the new instance (i.e.: mounted(), watchers, computed, store, you name it...).
I've set up a demo here: https://codesandbox.io/s/gifted-mestorf-297xx?file=/src/components/HelloWorld.vue
Notice I'm not doing the appendChild in the function purposefully, as in some cases I want to use the instance without adding it to DOM. The regular usage is:
const component = addProgrammaticComponent(this, SomeComponent, dataFn);
this.$el.appendChild(component.instance.$el);
Depending on what your dynamic component does, you might want to call .dispose() on it in parent's beforeDestroy(). If you don't, beforeDestroy() on child never gets called.
Probably the coolest part about it all is you don't actually need to append the child to the parent's DOM (it can be placed anywhere in DOM and the child will still respond to any changes of the parent, like it would if it was an actual descendant). Their "link" is programmatic, through dataFn.
Obviously, this opens the door to a bunch of potential problems, especially around destroying the parent without destroying the child. So you need be very careful and thorough about this type of cleanup. You either register each dynamic component into a property of the parent and .dispose() all of them in the parent's beforeDestroy() or give them a particular selector and sweep the entire DOM clean before destroying the parent.
Another interesting note is that in Vue 3 all of the above will no longer be necessary, as most of the core Vue functionality (reactivity, computed, hooks, listeners) is now exposed and reusable as is, so you won't have to $mount a component in order to have access to its "magic".

Vue - instance data is not defined

I have a menu of topics (e.g. "About us", "Support") and I want to be able to tell what topic is active right now. When a topic is clicked it will become the active one. Think of it as a substitute for a router that will set an active route. This is not possible in my case since I'm working in SharePoint 2010... in a content editor. So I made the decision to have an activeTopic variable in my Vue instance.
The Vue instance
new Vue({
el: '#app',
data: {
activeTopic: '',
}
});
This is because the activeTopic has to update another component and show some data based on the active topic. Like a router showing a child route.
The topic component
Vue.component('topic', {
props: ["title"],
methods: {
setActiveTopic: function (title) {
activeTopic = title;
},
isActiveTopic: function (title) {
return activeTopic == title;
}
},
template: `
<div v-bind:class="{active: isActiveTopic(title)}" v-on:click="setActiveTopic(title)">
<p v-text="title"></p>
</div>
`
});
The setActiveTopic function works as it should when I click it; it updates the activeTopic. But the isActiveTopic function gives me this error:
Error in render: "ReferenceError: activeTopic is not defined"
What I don't understand is that I can edit the activeTopic variable but I can't make a comparison? I've tried setting a default value but it still says it is undefined.
activeTopic should be assigned to the Vue component by setting and reading this.activeTopic. As is, you have two independent activeTopic variables scoped within each of your component methods.
You'll also want to include activeTopic in the component's data block, rather than on the Vue object itself, since it's specific to the component.
(If there's a reason to put that variable on the Vue object you would either want to pass it down to the component as a prop, or else access it directly as Vue.activeTopic instead of this.activeTopic. I'm honestly not certain whether Vue would treat that last structure as a reactive value within components -- I've never had a reason to try that, and can't think of a situation where that'd be a reasonable architecture.)
Vue.component('topic', {
props: ["title"],
data() { return {
activeTopic: ''
}},
methods: {
setActiveTopic(title) {
this.activeTopic = title;
},
isActiveTopic(title) {
return this.activeTopic == title;
}
},
template: `
<div v-bind:class="{active: isActiveTopic(title)}" v-on:click="setActiveTopic(title)">
<p v-text="title"></p>
</div>
`
});
I am new to Vue, and have seen data being defined as an object in several examples.
But apparently, as per the documentation, you have to use data as a function that returns an object.
This is to ensure that all instances will have it's own version of the data object, and is not shared with all instances.
Instead of
data: {
count: 0
}
use
data: function () {
return {
count: 0
}
}
However I still don't know why in one of my component, data as an object worked, and in a child component with it's own data as an object, I got the error count is undefined;
I am assuming, the root element, (defined by new Vue({}) has to be a singleton, and is not intended to have several instances.
And since components, can have instances, the data in that needs to be defined as a function.

What is the main difference between computed and mounted in vue.js 2?

when i add computed() instead mounted() it throws an error
export default {
components: {
MainLayout
},
mounted(){
var x = document.getElementById('homeTabPanes');
// x.style.background = "blue";
console.log("check the value of x", x);
}
}
computed is an object containing methods that returns data, mounted is a life hook executed after the instance gets mounted, check out the links to the docs it have really good explanation
From the docs
..computed properties are cached based on their dependencies. A computed property will only re-evaluate when some of its dependencies have changed.
If you want data to be cached use Computed properties on the other hand mounted is a lifecycle hook, a method which is called as soon as the Vue instance is mounted on the DOM.
In-template expressions are very convenient, but they are meant for simple operations. Putting too much logic in your templates can make them bloated and hard to maintain.
That’s why for any complex logic, you should use a computed property.
Basic Example
<div id="reverseMessageContainer">
<p>Original message: "{{ message }}"</p>
<p>Computed reversed message: "{{ reversedMessage }}"</p>
</div>
look at the js below:
var vm = new Vue({
el: '#reverseMessageContainer',
data: {
message: 'Hello'
},
computed: {
// a computed getter
reversedMessage: function () {
// `this` points to the vm instance
return this.message.split('').reverse().join('')
}
}
})
Here we have declared a computed property reversedMessage. The function we provided will be used as the getter function for the property vm.reversedMessage:
You can open the console and play with the example vm yourself. The value of vm.reversedMessage is always dependent on the value of vm.message.
console.log(vm.reversedMessage) // => 'olleH'
vm.message = 'Goodbye'
console.log(vm.reversedMessage) // => 'eybdooG'
For more better understanding you can visit
https://v2.vuejs.org/v2/guide/computed.html

Why does one Vue instance update with the data while another view instance doesn't?

I am using Vue for the first time, and am confused about why one of my Vue objects keeps updating when the data changes, and the other Vue object won't.
I have a JavaScript module like this to basically just hold data:
var MyModule = (function(){
const info = {
requestCount: 20,
arrayOfItems: []
}
return info;
})();
I have this html:
<span id='requestCounter' class='badge'>{{count}}</span>
<table id='vueTable'><tr v-for="item in items"><td>{{item.description}}</td></tr></table>
And lastly, I have this JavaScript file to do everything:
window.onload = function(){
var mainTable = new Vue({
el: '#vueTable',
data: {
items: MyModule.arrayOfItems
}
});
var requestCounterVue = new Vue({
el: '#requestCounter',
data: {
count: MyModule.requestCount
}
});
}
On the first runthrough, everything works as intended. Changing the MyModule.arrayOfItems works correctly, the table updates depending on what is in the array. However, changing MyModule.requestCount doesn't do anything. Can anybody tell me what I'm doing wrong here?
(Note, my example is simplified to make my question easier)
You are initializing the data in your Vue objects from MyModule. This does not make MyModule a proper store or controller. The weird behavior is not that requestCount doesn't react, but that arrayOfItems does.
The reason the array behaves as it does is that objects are passed by reference, so when you initialize items, Vue gets hold of the actual array object and does its magic on it. Effectively, MyModule provides an external handle to your Vue data.
requestCount, on the other hand, is a simple value, not an object, so it is passed by value, and there is absolutely no relationship between the member in MyModule and the data item in requestCounterVue.
Roy is exactly right. I'm just leaving this here because I was typing while he answered, and possibly more explanation couldn't hurt.
MyModule doesn't become reactive just because you use it to initialize values in your Vues. I expect the reason arrayOfItems is working the way you expect is because you are not actually setting it to a new array, you are mutating the existing array.
For example, your Vue using the array is defined here.
var mainTable = new Vue({
el: '#vueTable',
data: {
items: MyModule.arrayOfItems
}
});
If you add a value to MyModule.arrayOfItems like this
MyModule.arrayOfItems.push('my new value')
then your Vue will show the new value. This is because mainTable.items and MyModule.arrayOfItems are pointers to the same array.
If instead, however, you modified MyModule.arrayOfItems like this
MyModule.arrayOfItems = ['my','new','array']
Then mainTable will not show the updated array. This is because you've changed it to a completely different array. In other words, MyModule.arrayOfItems !== mainTable.items.
This is also the reason changing MyModule.count to a new value is not reflected in requestCounterVue. MyModule.count is a primitive value and when you initialized requestCounterVue with count, it used a copy of the value and made the copy reactive. Changing MyModule.count will have no effect.
If you wanted to see the changes reflected, what you might do is initialize your Vues this way
var mainTable = new Vue({
el: '#vueTable',
data: MyModule
});
When the Vue is instantiated, MyModule will be turned into a reactive object and changes will be reflected for both count and arrayOfItems.