Props passed to dynamic component in Vue are not reactive - vue.js

I am using Vue's dynamic component to load a component depending on what is to be displayed. As these components all have different props I build an object and pass it in via v-bind depending on what I need to use from the original state model.
However, when I do this I lose the reactive nature of Vue's props data flow. The code sample below shows an example of this with the name changing on the standard component but not on the dynamic version.
I expect this is something to do with the string value being copied into the new object, rather than a reference to the original reactive property. Can anyone advise on how I can make this work as expected?
Vue.config.productionTip = false;
Vue.component("example-component", {
props: ["name"],
template: '<span style="color: green;">{{name}}</span>'
}
);
var app = new Vue({
el: "#app",
data: {
person: {
name: "William"
},
component: null
}
});
// Load the dynamic component
setTimeout(function() {
app.component = {
is: 'example-component',
props: { name: app.person.name }
}
// Change the name
setTimeout(function() {
app.person.name = "Sarah";
}, 2000);
}, 2000);
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="app">
<strong>Standard Component</strong><br />
<example-component :name="person.name"></example-component><br /><br />
<div v-if="component">
<strong>Dynamic Component</strong><br />
<component :is="component.is" v-bind="component.props"></component>
</div>
</div>

There's two different objects here, person and component, which are declared in the app component's data property.
It is app.component.props.name which is bound to the component's prop, but you are modifying app.person.name which is separate and not bound to the component.
app.component.props and app.person are two separate object instances, and so modifying one of these object's properties will have no effect on the other.
It's difficult to suggest a suitable solution to your problem because your example is too simple (and a little contrived). What you want will not work so long as you are copying the name value between different objects.
I'd redo all of the code, and perhaps use a computed property instead. But with the least amount of changes you can do this:
app.component = {
is: 'example-component',
props: {
get name() { return app.person.name; }
}
}
Now app.component.props.name is actually a getter function which returns app.person.name. Vue can observe this, and will react when app.person.name changes.

Related

Unexpected mutation of prop in Vue2 [duplicate]

I started https://laracasts.com/series/learning-vue-step-by-step series. I stopped on the lesson Vue, Laravel, and AJAX with this error:
vue.js:2574 [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: "list" (found in component )
I have this code in main.js
Vue.component('task', {
template: '#task-template',
props: ['list'],
created() {
this.list = JSON.parse(this.list);
}
});
new Vue({
el: '.container'
})
I know that the problem is in created() when I overwrite the list prop, but I am a newbie in Vue, so I totally don't know how to fix it. Does anyone know how (and please explain why) to fix it?
This has to do with the fact that mutating a prop locally is considered an anti-pattern in Vue 2
What you should do now, in case you want to mutate a prop locally, is to declare a field in your data that uses the props value as its initial value and then mutate the copy:
Vue.component('task', {
template: '#task-template',
props: ['list'],
data: function () {
return {
mutableList: JSON.parse(this.list);
}
}
});
You can read more about this on Vue.js official guide
Note 1: Please note that you should not use the same name for your prop and data, i.e.:
data: function () { return { list: JSON.parse(this.list) } } // WRONG!!
Note 2: Since I feel there is some confusion regarding props and reactivity, I suggest you to have a look on this thread
The Vue pattern is props down and events up. It sounds simple, but is easy to forget when writing a custom component.
As of Vue 2.2.0 you can use v-model (with computed properties). I have found this combination creates a simple, clean, and consistent interface between components:
Any props passed to your component remains reactive (i.e., it's not cloned nor does it require a watch function to update a local copy when changes are detected).
Changes are automatically emitted to the parent.
Can be used with multiple levels of components.
A computed property permits the setter and getter to be separately defined. This allows the Task component to be rewritten as follows:
Vue.component('Task', {
template: '#task-template',
props: ['list'],
model: {
prop: 'list',
event: 'listchange'
},
computed: {
listLocal: {
get: function() {
return this.list
},
set: function(value) {
this.$emit('listchange', value)
}
}
}
})
The model property defines which prop is associated with v-model, and which event will be emitted on changes. You can then call this component from the parent as follows:
<Task v-model="parentList"></Task>
The listLocal computed property provides a simple getter and setter interface within the component (think of it like being a private variable). Within #task-template you can render listLocal and it will remain reactive (i.e., if parentList changes it will update the Task component). You can also mutate listLocal by calling the setter (e.g., this.listLocal = newList) and it will emit the change to the parent.
What's great about this pattern is that you can pass listLocal to a child component of Task (using v-model), and changes from the child component will propagate to the top level component.
For example, say we have a separate EditTask component for doing some type of modification to the task data. By using the same v-model and computed properties pattern we can pass listLocal to the component (using v-model):
<script type="text/x-template" id="task-template">
<div>
<EditTask v-model="listLocal"></EditTask>
</div>
</script>
If EditTask emits a change it will appropriately call set() on listLocal and thereby propagate the event to the top level. Similarly, the EditTask component could also call other child components (such as form elements) using v-model.
Vue just warns you: you change the prop in the component, but when parent component re-renders, "list" will be overwritten and you lose all your changes. So it is dangerous to do so.
Use computed property instead like this:
Vue.component('task', {
template: '#task-template',
props: ['list'],
computed: {
listJson: function(){
return JSON.parse(this.list);
}
}
});
If you're using Lodash, you can clone the prop before returning it. This pattern is helpful if you modify that prop on both the parent and child.
Let's say we have prop list on component grid.
In Parent Component
<grid :list.sync="list"></grid>
In Child Component
props: ['list'],
methods:{
doSomethingOnClick(entry){
let modifiedList = _.clone(this.list)
modifiedList = _.uniq(modifiedList) // Removes duplicates
this.$emit('update:list', modifiedList)
}
}
Props down, events up. That's Vue's Pattern. The point is that if you try to mutate props passing from a parent. It won't work and it just gets overwritten repeatedly by the parent component. Child component can only emit an event to notify parent component to do sth. If you don't like these restrict, you can use VUEX(actually this pattern will suck in complex components structure, you should use VUEX!)
You should not change the props's value in child component.
If you really need to change it you can use .sync.
Just like this
<your-component :list.sync="list"></your-component>
Vue.component('task', {
template: '#task-template',
props: ['list'],
created() {
this.$emit('update:list', JSON.parse(this.list))
}
});
new Vue({
el: '.container'
})
According to the VueJs 2.0, you should not mutate a prop inside the component. They are only mutated by their parents. Therefore, you should define variables in data with different names and keep them updated by watching actual props.
In case the list prop is changed by a parent, you can parse it and assign it to mutableList. Here is a complete solution.
Vue.component('task', {
template: ´<ul>
<li v-for="item in mutableList">
{{item.name}}
</li>
</ul>´,
props: ['list'],
data: function () {
return {
mutableList = JSON.parse(this.list);
}
},
watch:{
list: function(){
this.mutableList = JSON.parse(this.list);
}
}
});
It uses mutableList to render your template, thus you keep your list prop safe in the component.
The answer is simple, you should break the direct prop mutation by assigning the value to some local component variables(could be data property, computed with getters, setters, or watchers).
Here's a simple solution using the watcher.
<template>
<input
v-model="input"
#input="updateInput"
#change="updateInput"
/>
</template>
<script>
export default {
props: {
value: {
type: String,
default: '',
},
},
data() {
return {
input: '',
};
},
watch: {
value: {
handler(after) {
this.input = after;
},
immediate: true,
},
},
methods: {
updateInput() {
this.$emit('input', this.input);
},
},
};
</script>
It's what I use to create any data input components and it works just fine. Any new data sent(v-model(ed)) from parent will be watched by the value watcher and is assigned to the input variable and once the input is received, we can catch that action and emit input to parent suggesting that data is input from the form element.
do not change the props directly in components.if you need change it set a new property like this:
data() {
return {
listClone: this.list
}
}
And change the value of listClone.
I faced this issue as well. The warning gone after i use $on and $emit.
It's something like use $on and $emit recommended to sent data from child component to parent component.
one-way Data Flow,
according to https://v2.vuejs.org/v2/guide/components.html, the component follow 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, this prevents child components from accidentally mutating the parent's, which can make your app's data flow harder to understand.
In addition, every time the parent component is updates all props
in the child components will be refreshed with the latest value. This means you should not attempt to mutate a prop inside a child component. If you do .vue will warn you in the
console.
There are usually two cases where it’s tempting to mutate a prop:
The prop is used to pass in an initial value; the child component wants to use it as a local data property afterwards.
The prop is passed in as a raw value that needs to be transformed.
The proper answer to these use cases are:
Define a local data property that uses the prop’s initial value as its initial value:
props: ['initialCounter'],
data: function () {
return { counter: this.initialCounter }
}
Define a computed property that is computed from the prop’s value:
props: ['size'],
computed: {
normalizedSize: function () {
return this.size.trim().toLowerCase()
}
}
If you want to mutate props - use object.
<component :model="global.price"></component>
component:
props: ['model'],
methods: {
changeValue: function() {
this.model.value = "new value";
}
}
I want to give this answer which helps avoid using a lot of code, watchers and computed properties. In some cases this can be a good solution:
Props are designed to provide one-way communication.
When you have a modal show/hide button with a prop the best solution to me is to emit an event:
<button #click="$emit('close')">Close Modal</button>
Then add listener to modal element:
<modal :show="show" #close="show = false"></modal>
(In this case the prop show is probably unnecessary because you can use an easy v-if="show" directly on the base-modal)
You need to add computed method like this
component.vue
props: ['list'],
computed: {
listJson: function(){
return JSON.parse(this.list);
}
}
Vue.component('task', {
template: '#task-template',
props: ['list'],
computed: {
middleData() {
return this.list
}
},
watch: {
list(newVal, oldVal) {
console.log(newVal)
this.newList = newVal
}
},
data() {
return {
newList: {}
}
}
});
new Vue({
el: '.container'
})
Maybe this will meet your needs.
Vue3 has a really good solution. Spent hours to reach there. But it worked really good.
On parent template
<user-name
v-model:first-name="firstName"
v-model:last-name="lastName"
></user-name>
The child component
app.component('user-name', {
props: {
firstName: String,
lastName: String
},
template: `
<input
type="text"
:value="firstName"
#input="$emit('update:firstName',
$event.target.value)">
<input
type="text"
:value="lastName"
#input="$emit('update:lastName',
$event.target.value)">
`
})
This was the only solution which did two way binding. I like that first two answers were addressing in good way to use SYNC and Emitting update events, and compute property getter setter, but that was heck of a Job to do and I did not like to work so hard.
Vue.js props are not to be mutated as this is considered an Anti-Pattern in Vue.
The approach you will need to take is creating a data property on your component that references the original prop property of list
props: ['list'],
data: () {
return {
parsedList: JSON.parse(this.list)
}
}
Now your list structure that is passed to the component is referenced and mutated via the data property of your component :-)
If you wish to do more than just parse your list property then make use of the Vue component' computed property.
This allow you to make more in depth mutations to your props.
props: ['list'],
computed: {
filteredJSONList: () => {
let parsedList = JSON.parse(this.list)
let filteredList = parsedList.filter(listItem => listItem.active)
console.log(filteredList)
return filteredList
}
}
The example above parses your list prop and filters it down to only active list-tems, logs it out for schnitts and giggles and returns it.
note: both data & computed properties are referenced in the template the same e.g
<pre>{{parsedList}}</pre>
<pre>{{filteredJSONList}}</pre>
It can be easy to think that a computed property (being a method) needs to be called... it doesn't
For when TypeScript is your preferred lang. of development
<template>
<span class="someClassName">
{{feesInLocale}}
</span>
</template>
#Prop({default: 0}) fees: any;
// computed are declared with get before a function
get feesInLocale() {
return this.fees;
}
and not
<template>
<span class="someClassName">
{{feesInLocale}}
</span>
</template>
#Prop() fees: any = 0;
get feesInLocale() {
return this.fees;
}
Assign the props to new variable.
data () {
return {
listClone: this.list
}
}
Adding to the best answer,
Vue.component('task', {
template: '#task-template',
props: ['list'],
data: function () {
return {
mutableList: JSON.parse(this.list);
}
}
});
Setting props by an array is meant for dev/prototyping, in production make sure to set prop types(https://v2.vuejs.org/v2/guide/components-props.html) and set a default value in case the prop has not been populated by the parent, as so.
Vue.component('task', {
template: '#task-template',
props: {
list: {
type: String,
default() {
return '{}'
}
}
},
data: function () {
return {
mutableList: JSON.parse(this.list);
}
}
});
This way you atleast get an empty object in mutableList instead of a JSON.parse error if it is undefined.
YES!, mutating attributes in vue2 is an anti-pattern. BUT...
Just break the rules by using other rules, and go forward!
What you need is to add .sync modifier to your component attribute in the parent scope.
<your-awesome-components :custom-attribute-as-prob.sync="value" />
Below is a snack bar component, when I give the snackbar variable directly into v-model like this if will work but in the console, it will give an error as
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.
<template>
<v-snackbar v-model="snackbar">
{{ text }}
</v-snackbar>
</template>
<script>
export default {
name: "loader",
props: {
snackbar: {type: Boolean, required: true},
text: {type: String, required: false, default: ""},
},
}
</script>
Correct Way to get rid of this mutation error is use watcher
<template>
<v-snackbar v-model="snackbarData">
{{ text }}
</v-snackbar>
</template>
<script>
/* eslint-disable */
export default {
name: "loader",
data: () => ({
snackbarData:false,
}),
props: {
snackbar: {type: Boolean, required: true},
text: {type: String, required: false, default: ""},
},
watch: {
snackbar: function(newVal, oldVal) {
this.snackbarData=!this.snackbarDatanewVal;
}
}
}
</script>
So in the main component where you will load this snack bar you can just do this code
<loader :snackbar="snackbarFlag" :text="snackText"></loader>
This Worked for me
Vue.js considers this an anti-pattern. For example, declaring and setting some props like
this.propsVal = 'new Props Value'
So to solve this issue you have to take in a value from the props to the data or the computed property of a Vue instance, like this:
props: ['propsVal'],
data: function() {
return {
propVal: this.propsVal
};
},
methods: {
...
}
This will definitely work.
In addition to the above, for others having the following issue:
"If the props value is not required and thus not always returned, the passed data would return undefined (instead of empty)". Which could mess <select> default value, I solved it by checking if the value is set in beforeMount() (and set it if not) as follows:
JS:
export default {
name: 'user_register',
data: () => ({
oldDobMonthMutated: this.oldDobMonth,
}),
props: [
'oldDobMonth',
'dobMonths', //Used for the select loop
],
beforeMount() {
if (!this.oldDobMonth) {
this.oldDobMonthMutated = '';
} else {
this.oldDobMonthMutated = this.oldDobMonth
}
}
}
Html:
<select v-model="oldDobMonthMutated" id="dob_months" name="dob_month">
<option selected="selected" disabled="disabled" hidden="hidden" value="">
Select Month
</option>
<option v-for="dobMonth in dobMonths"
:key="dobMonth.dob_month_slug"
:value="dobMonth.dob_month_slug">
{{ dobMonth.dob_month_name }}
</option>
</select>
I personally always suggest if you are in need to mutate the props, first pass them to computed property and return from there, thereafter one can mutate the props easily, even at that you can track the prop mutation , if those are being mutated from another component too or we can you watch also .
Because Vue props is one way data flow, This prevents child components from accidentally mutating the parent’s state.
From the official Vue document, we will find 2 ways to solve this problems
if child component want use props as local data, it is best to define a local data property.
props: ['list'],
data: function() {
return {
localList: JSON.parse(this.list);
}
}
The prop is passed in as a raw value that needs to be transformed. In this case, it’s best to define a computed property using the prop’s value:
props: ['list'],
computed: {
localList: function() {
return JSON.parse(this.list);
},
//eg: if you want to filter this list
validList: function() {
return this.list.filter(product => product.isValid === true)
}
//...whatever to transform the list
}
You should always avoid mutating props in vue, or any other framework. The approach you could take is copy it into another variable.
for example.
// instead of replacing the value of this.list use a different variable
this.new_data_variable = JSON.parse(this.list)
A potential solution to this is using global variables.
import { Vue } from "nuxt-property-decorator";
export const globalStore = new Vue({
data: {
list: [],
},
}
export function setupGlobalsStore() {
Vue.prototype.$globals = globalStore;
}
Then you would use:
$globals.list
Anywhere you need to mutate it or present it.

Vue: Make sure the component cannot modify props (even if it is a reference object)?

I know that the vue model is a unidirectional data flow of props.
However, when prop is a reference object, the component can directly modify its properties. This is wrong, but vue will not check it.
I hope there is a mechanism to ensure that the component cannot modify the props (even if it is a reference object), rather than being checked by the developer.
For example, I have a component
<template>
<input v-model="obj.text" />
</template>
<script>
export default {
props: ['obj']
};
</script>
And a page that uses it
<template>
<my-template :obj="myobj"></my-template>
</template>
<script>
export default {
data() {
myobj: {
text: "hello";
}
}
};
</script>
When data changes in 'input', myobj.text will change together. This violates the unidirectional data flow.
Of course, as shown in the answer, I can use the "get" and "set" methods of the "computed".
But I must be careful not to write 'obj.someProperty' to any 'v-model', but this requires my own attention.
I hope there is a mechanism to give a hint when I make a mistake.
Couldn't find an existing duplicate so here's an answer. If anyone can find one, let me know and I'll make this one a Community wiki.
Use a computed property with getter and setter to represent your v-model value.
The getter gets the value from the prop and the setter emits the new value to the parent.
For example
<input v-model="computedProp">
props: ['referenceObject'],
computed: {
computedProp: {
get () {
return this.referenceObject.someProperty
},
set (val) {
this.$emit('updated', val)
}
}
}
and in the parent
<SomeComponent :reference-object="refObject" #updated="updateRefObject">
data: () => ({ refObject: { someProperty: 'initial value' } }),
methods: {
updateRefObject (newVal) {
this.refObject.someProperty = newVal
}
}

Attach v-model to a dynamic element added with .appendChild in Vue.js

I'm working with a library that doesn't have a Vue.js wrapper.
The library appends elements in the DOM in a dynamic way.
I want to be able to bind the v-model attribute to those elements with Vue and once appended work with them in my model.
I've done this in the past with other reactive frameworks such as Knockout.js, but I can't find a way to do it with vue.js.
Any pay of this doing?
It should be something among these lines I assume:
var div = document.createElement('div');
div.setAttribute('v-model', '{{demo}}');
[VUE CALL] //tell vue.js I want to use this element in my model.
document.body.appendChild(div);
You could create a wrapper component for your library and then setup custom v-model on it to get a result on the lines of what you're looking for. Since your library is in charge of manipulating the DOM, you'd have to hook into the events provided by your library to ensure your model is kept up-to-date. You can have v-model support for your component by ensuring two things:
It accepts a value prop
It emits an input event
Here's an example of doing something similar: https://codesandbox.io/s/listjs-jquery-wrapper-sdli1 and a snipper of the wrapper component I implemented:
<template>
<div>
<div ref="listEl">
<ul ref="listUlEl" class="list"></ul>
</div>
<div class="form">
<div v-for="variable in variables" :key="variable">
{{ variable }}
<input v-model="form[variable]" placeholder="Enter a value">
</div>
<button #click="add()">Add</button>
</div>
</div>
</template>
<script>
export default {
props: ["value", "variables", "template"],
data() {
return {
form: {}
};
},
mounted() {
this.list = new List(
this.$refs.listEl,
{
valueNames: this.variables,
item: this.template
},
this.value
);
this.createFormModels();
},
methods: {
createFormModels() {
for (const variable of this.variables) {
this.$set(this.form, variable, "");
}
},
add() {
this.$emit("input", [
...this.value,
{
id: this.value.slice(-1)[0].id + 1,
...this.form
}
]);
}
},
watch: {
value: {
deep: true,
handler() {
this.$refs.listUlEl.innerHTML = "";
this.list = new List(
this.$refs.listEl,
{
valueNames: this.variables,
item: this.template
},
this.value
);
}
}
},
beforeDestroy() {
// Do cleanup, eg:
// this.list.destroy();
}
};
</script>
Key points:
Do your initialization of the custom library on mounted() in order to create the DOM. If it needs an element to work with, provide one via <template> and put a ref on it. This is also the place to setup event listeners on your library so that you can trigger model updates via $emit('value', newListOfStuff).
watch for changes to the value prop so that you can reinitialize the library or if it provides a way to update its collection, use that instead. Make sure to cleanup the previous instance if the library provides support for it as well as unbind event handlers.
Call any cleanup operations, event handler removals inside beforeDestroy.
For further reference:
https://v2.vuejs.org/v2/guide/components.html#Using-v-model-on-Components

How to clone props object and make it non-reactive [duplicate]

This question already has answers here:
What is the most efficient way to deep clone an object in JavaScript?
(67 answers)
Closed 12 days ago.
I have some form data which I share with children components through props. Now I want to clone the prop object and make it non-reactive. In my case I want the user to be able to modify the props value without actually changing the cloned value. The cloned value should only be there to show the user what the form was when editing. Below code shows this:
<template>
<div>
<div v-if="computedFormData">
original prop title: {{orgData.title}}
new title:
<input type="text" v-model="formData.title"/>
//changing data here will also change orgData.title
</div>
</div>
</template>
<script>
export default {
props: ['formData'],
data() {
return {
orgData: [],
}
},
computed: {
computedFormData: function () {
this.orgData = this.formData;
return this.orgData;
},
},
methods: {
},
}
</script>
I have tried with Object.freeze(testData); but it doesnt work, both testData and orgData are reactive. Note also that using mounted or created property does not render orgData so I'm forced to use the computed property.
Try copying the prop values with Object.assign. No more issue with reactivity since the new, assigned values are just the copy instead of the reference to the source.
If your data object is a lot more complex, I'd recommend deepmerge in place of Object.assign.
Vue.component('FormData', {
template: `
<div>
<div v-if="testData">
<p>Original prop title: <strong>{{orgData.title}}</strong></p>
<p>Cloned prop title:</p>
<input type="text" v-model="testData.title" />
</div>
</div>
`,
props: ['orgData'],
data() {
return {
testData: Object.assign({}, this.orgData)
}
}
});
const vm = new Vue({
el: '#app',
data() {
return {
dummyForm: {
title: 'Some title'
}
}
}
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="app">
<form-data :org-data="dummyForm"></form-data>
</div>
Not entirely sure why but using Object.assign on a computed property did not work for me. I solved it by using a watch property for the props value:
watch:{
formData(){
this.orgData = Object.assign({}, this.formData)
}
},
Object.assign is merely a shallow copy. If you have a copy consists that of only primitive data types (string, number, bigint, boolean, undefined, symbol, and null) it's ok. It to remove its reactivity. But, if you have a copy that has reference types you can’t shallow clone it to remove its reactivity.
For depping clone you can use the JSON.parse(JSON.stringify()) pattern. But keep in mind that is going to work if your data consists of supported JSON data types.
props: ['orgData'],
data() {
return {
cloneOrgData: JSON.parse(JSON.stringify(this.orgData))
}
}

How to defer passing of Ajax data to child components?

I'm building a simple to-do app with Vue 2.0 using built-in parent-child communication. The parent element that is directly attached to the Vue instance (new Vue({...}) is as follows:
<!-- Tasks.vue -->
<template>
<div>
<create-task-form></create-task-form>
<task-list :tasks="tasks"></task-list>
<task-list :tasks="incompleteTasks"></task-list>
<task-list :tasks="completeTasks"></task-list>
</template>
<script>
import CreateTaskForm from './CreateTaskForm.vue';
import TaskList from './TaskList.vue';
export default {
components: { CreateTaskForm, TaskList },
data() { return { tasks: [] }; },
created() { // Ajax call happens here...
axios.get('/api/v1/tasks')
.then(response => {
this.tasks = response.data;
console.log(this.tasks); // THIS IS LOGGED LAST
});
},
computed: {
completeTasks() {
return this.tasks.filter(task => task.complete);
},
incompleteTasks() {
return this.tasks.filter(task => task.complete);
}
}
}
</script>
The idea is that <tasks></tasks> will display a form to create a new task, as well 3 lists - all tasks, incomplete, and complete tasks. Each list is the same component:
<!-- TaskList.vue -->
<template>
<ul>
<li v-for="task in taskList">
<input type="checkbox" v-model="task.complete"> {{ task.name }}
</li>
</ul>
</template>
<script>
export default {
data() { return { taskList: [] }; },
props: ['tasks'],
mounted() {
this.taskList = this.tasks;
console.log(this.tasks); // THIS IS LOGGED FIRST
}
}
</script>
Problem
As you can see, I am trying to pass data from <tasks> to each of the 3 <task-lists>, using dynamic :tasks property:
<task-list :tasks="tasks"></task-list>
<task-list :tasks="incompleteTasks"></task-list>
<task-list :tasks="completeTasks"></task-list>
Note, I am not using shared (global) state, because each list needs a different portion of data, even though these portions of data belong to the same store. But the problem is that :tasks are assigned an empty array before the Ajax call happens; and as I am guessing, props are immutable, hence tasks in the child <task-list> are never updated when data is fetched in the parent <tasks>. In fact, <task-list> is created first (see the log), and only then the data is fetched using Ajax.
Questions
How do I defer the passing of data from <tasks> to my <task-list>s? How do I make sure that all components refer to the single source of truth that's updated dynamically?
Is there a way to solve this parent-child communication problem with "vanilla" Vue.js? Or do I need to use Vuex or something similar?
Am I right in using properties to pass data to children? Or should I use shared store in a global variable?
The problem is here: mounted() { this.taskList = this.tasks; ...} inside TaskList.vue as taskList property is updated only on mounted event.
There's a trivial solution in Vue: you should make taskList a computed property which depends on props, so that when parent data changes your computed property gets updated:
props: ['tasks'],
computed: {
taskList: function() {
return this.tasks;
}
}
Don't forget to remove taskList from data block.
Also, I would rewrite v-model="task.complete" into #change="$emit('something', task.id)" to inform a parent component that status has changed (and listen to ). Otherwise parent will never know the box is checked. You can then listen for this event on parent component to change tasks status accordingly. More reading: https://v2.vuejs.org/v2/guide/components.html#Custom-Events
You can also use watch property of vue instance
watch:{
tasks(newVal){
this.taskList = newVal;
//do something
}
}