How to Watch Props Change with Vue Composition API / Vue 3? - javascript

While Vue Composition API RFC Reference site has many advanced use scenarios with the watch module, there is no examples on how to watch component props?
Neither is it mentioned in Vue Composition API RFC's main page or vuejs/composition-api in Github.
I've created a Codesandbox to elaborate this issue.
<template>
<div id="app">
<img width="25%" src="./assets/logo.png">
<br>
<p>Prop watch demo with select input using v-model:</p>
<PropWatchDemo :selected="testValue"/>
</div>
</template>
<script>
import { createComponent, onMounted, ref } from "#vue/composition-api";
import PropWatchDemo from "./components/PropWatchDemo.vue";
export default createComponent({
name: "App",
components: {
PropWatchDemo
},
setup: (props, context) => {
const testValue = ref("initial");
onMounted(() => {
setTimeout(() => {
console.log("Changing input prop value after 3s delay");
testValue.value = "changed";
// This value change does not trigger watchers?
}, 3000);
});
return {
testValue
};
}
});
</script>
<template>
<select v-model="selected">
<option value="null">null value</option>
<option value>Empty value</option>
</select>
</template>
<script>
import { createComponent, watch } from "#vue/composition-api";
export default createComponent({
name: "MyInput",
props: {
selected: {
type: [String, Number],
required: true
}
},
setup(props) {
console.log("Setup props:", props);
watch((first, second) => {
console.log("Watch function called with args:", first, second);
// First arg function registerCleanup, second is undefined
});
// watch(props, (first, second) => {
// console.log("Watch props function called with args:", first, second);
// // Logs error:
// // Failed watching path: "[object Object]" Watcher only accepts simple
// // dot-delimited paths. For full control, use a function instead.
// })
watch(props.selected, (first, second) => {
console.log(
"Watch props.selected function called with args:",
first,
second
);
// Both props are undefined so its just a bare callback func to be run
});
return {};
}
});
</script>
EDIT: Although my question and code example was initially with JavaScript, I'm actually using TypeScript. Tony Tom's first answer although working, lead to a type error. Which was solved by Michal Levý's answer. So I've tagged this question with typescript afterwards.
EDIT2: Here is my polished yet barebones version of the reactive wirings for this custom select component, on top of <b-form-select> from bootstrap-vue (otherwise agnostic-implementation but this underlying component does emit #input and #change events both, based on whether change was made programmatically or by user interaction).
<template>
<b-form-select
v-model="selected"
:options="{}"
#input="handleSelection('input', $event)"
#change="handleSelection('change', $event)"
/>
</template>
<script lang="ts">
import {
createComponent, SetupContext, Ref, ref, watch, computed,
} from '#vue/composition-api';
interface Props {
value?: string | number | boolean;
}
export default createComponent({
name: 'CustomSelect',
props: {
value: {
type: [String, Number, Boolean],
required: false, // Accepts null and undefined as well
},
},
setup(props: Props, context: SetupContext) {
// Create a Ref from prop, as two-way binding is allowed only with sync -modifier,
// with passing prop in parent and explicitly emitting update event on child:
// Ref: https://v2.vuejs.org/v2/guide/components-custom-events.html#sync-Modifier
// Ref: https://medium.com/#jithilmt/vue-js-2-two-way-data-binding-in-parent-and-child-components-1cd271c501ba
const selected: Ref<Props['value']> = ref(props.value);
const handleSelection = function emitUpdate(type: 'input' | 'change', value: Props['value']) {
// For sync -modifier where 'value' is the prop name
context.emit('update:value', value);
// For #input and/or #change event propagation
// #input emitted by the select component when value changed <programmatically>
// #change AND #input both emitted on <user interaction>
context.emit(type, value);
};
// Watch prop value change and assign to value 'selected' Ref
watch(() => props.value, (newValue: Props['value']) => {
selected.value = newValue;
});
return {
selected,
handleSelection,
};
},
});
</script>

If you take a look at watch typing here it's clear the first argument of watch can be array, function or Ref<T>
props passed to setup function is reactive object (made probably by readonly(reactive()), it's properties are getters. So what you doing is passing the value of the getter as the 1st argument of watch - string "initial" in this case. Because Vue 2 $watch API is used under the hood (and same function exists in Vue 3), you are effectively trying to watch non-existent property with name "initial" on your component instance.
Your callback is called only once and never again. Reason it is called at least once is because new watch API is behaving like current $watch with immediate option (UPDATE 03/03/2021 - this was later changed and in release version of Vue 3, watch is lazy same way as it was in Vue 2)
So by accident you doing the same thing Tony Tom suggested but with wrong value. In both cases it's not valid code if you are using TypeScript
You can do this instead:
watch(() => props.selected, (first, second) => {
console.log(
"Watch props.selected function called with args:",
first,
second
);
});
Here the 1st function is executed immediately by Vue to collect dependencies (to know what should trigger the callback) and 2nd function is the callback itself.
Other way would be to convert props object using toRefs so it's properties would be of type Ref<T> and you can pass them as a 1st argument of watch
Anyway, most of the time watching props is just not needed - simply use props.xxx directly in your template (or setup) and let the Vue do the rest

I just wanted to add some more details to the answer above. As Michal mentioned, the props coming is an object and is reactive as a whole. But, each key in the props object is not reactive on its own.
We need to adjust the watch signature for a value in the reactive object compared to a ref value
// watching value of a reactive object (watching a getter)
watch(() => props.selected, (selection, prevSelection) => {
/* ... */
})
// directly watching a ref
const selected = ref(props.selected)
watch(selected, (selection, prevSelection) => {
/* ... */
})
Just some more info even though it's not the mentioned case in the question:
If we want to watch on multiple properties, one can pass an array instead of a single reference
// Watching Multiple Sources
watch([ref1, ref2, ...], ([refVal1, refVal2, ...],[prevRef1, prevRef2, ...]) => {
/* ... */
})

This does not address the question of how to "watch" properties. But if you want to know how to make props responsive with Vue's Composition API, then read on. In most cases you shouldn't have to write a bunch of code to "watch" things (unless you're creating side effects after changes).
The secret is this: Component props IS reactive. As soon as you access a particular prop, it is NOT reactive. This process of dividing out or accessing a part of an object is referred to as "destructuring". In the new Composition API you need to get used to thinking about this all the time--it's a key part of the decision to use reactive() vs ref().
So what I'm suggesting (code below) is that you take the property you need and make it a ref if you want to preserve reactivity:
export default defineComponent({
name: 'MyAwesomestComponent',
props: {
title: {
type: String,
required: true,
},
todos: {
type: Array as PropType<Todo[]>,
default: () => [],
},
...
},
setup(props){ // this is important--pass the root props object in!!!
...
// Now I need a reactive reference to my "todos" array...
var todoRef = toRefs(props).todos
...
// I can pass todoRef anywhere, with reactivity intact--changes from parents will flow automatically.
// To access the "raw" value again:
todoRef.value
// Soon we'll have "unref" or "toRaw" or some official way to unwrap a ref object
// But for now you can just access the magical ".value" attribute
}
}
I sure hope the Vue wizards can figure out how to make this easier... but as far as I know this is the type of code we'll have to write with the Composition API.
Here is a link to the official documentation, where they caution you directly against destructuring props.

In my case I solved it using key
<MessageEdit :key="message" :message="message" />
Maybe on your case would look something like this
<PropWatchDemo :key="testValue" :selected="testValue"/>
But I don't have any idea of its pros and cons versus watch

Change your watch method like below.
watch("selected", (first, second) => {
console.log(
"Watch props.selected function called with args:",
first,second
);
// Both props are undefined so its just a bare callback func to be run
});

None of the options above worked for me but I think I found a simple way that seems to works very well to keep vue2 coding style in composition api
Simply create a ref alias to the prop like:
myPropAlias = ref(props.myProp)
and you do everything from the alias
works like a charm for me and minimal

Related

Vue.js: Is changing objects passed as props a bad technique?

I noticed that in Vue you could change object fields passed as Props, and it changes these fields in parent.
Is this a bad technique? Should it be avoided, or it may be used? What the caveats of this approach?
Example:
Parent.vue:
<script setup>
import { reactive } from 'vue';
import ChildInput from "./Child.vue";
</script>
<script>
const myObj = reactive({'val':'Default text'});
</script>
<template>
<ChildInput :passedObj="myObj" />
<div>
It's a parent input:
<input v-model="myObj.val">
</div>
</template>
Child.vue:
<script setup>
const props = defineProps({
passedObj: {
type: Object,
required: true,
},
})
const dropValue = function() {
props.passedObj.val = "Changed text";
}
</script>
<template>
<div>
<label>
It's a child input:
<input v-model="passedObj.val">
<button #click="dropValue">Change text</button>
</label>
</div>
</template>
You can check this example here.
Shallow prop mutation is prohibited because props object is read-only.
Deep prop mutation is a bad practice that should be avoided. One reason is that this makes data flow more complicated and hard to follow, that this happened unconsciously in this case explains why this is a problem. Another possible reason is that the performance can potentially be affected because this scenario lies outside the common use, although I'm unaware of such optimization problems as of now.
The official recommendation is to use v-model two-way binding when props need to be mutated, so the mutation happens in parent component and can be tracked through Vue events when debugging is needed. When a prop is deeply mutated, it's cloned in a child and emitted to a parent.
So it seems that it is a bad practice. I'll leave an alternative that I use myself, if someone needs one written with Composition API.
If you want to modify only certain fields of a prop, you can use computed variable with setter and getter. Like this.
In parent:
<script setup>
import { reactive } from "vue";
import InteractiveTable from "./Child.vue";
// Reactive object, which contains our data
const myObj = reactive({
data: [{name: "Sasha", address: "Middle of nowhere"}]
});
</script>
<InteractiveTable v-model="myObj" />
Remember that you may pass multiple v-model with custom titles: v-model:title. You can also instead listen for events by yourself. Like that:
<InteractiveTable :passedObj="myObj" #update:passedObj="(data) => <something>" />
In child:
import { computed } from "vue";
// Defining our props
// For v-model default prop is 'modelValue'
// Don't forget to assign it to a variable
const props = defineProps({
modelValue: {
type: Object,
required: true,
},
});
// Emits must be assigned to a variable too
const emit = defineEmits([
"update:modelValue",
]);
// our computed variable, which we will use in JS instead of prop
const localData = computed({
get() {
// note that we can use object itself or any of its properties, 'data' in this case
return props.modelValue.data;
},
set(val) {
emit("update:modelValue.data", val);
},
});
Now you can do:
const addRow = function (name, address) {
// You have to call this localData with .value
// In all other regards you can treat it as a normal variable
localData.value.push({
'name': name,
'address': address,
});
};
You can read more about computed variables here and about emit here.
I'm pretty new in Vue.js, so if you notice something wrong in my code, please notify me in comments.

Vue 2 not setting component data when page is refreshed

I am having a rather strange problem with vue (v2.6.14) in which I am creating a new array based on one receive as a prop. Here's the relevant code:
props: { employees: Array },
data() {
return {
sortedEmployees: [],
};
},
mounted() {
this.sortedEmployees = this.employees.slice(0);
},
Essencially what I want here is to create a new reference for the employees array so that I can sort it to display later without actually altering the original array. I am not worried about cloning the actual objects inside the array since I will not alter them.
The problem with this is that, when the app 'hot reloads'(due to some change in the code) it works as expected, the hook is called and the component data is set according to what is expected BUT if I actually refresh the page in the browser, even though the hook is called, the component data is not set and I end up with an empty array. I can solve this by setting up a watcher to the prop and then it would set the data there, but what I am interested here is understanding what's happening. If the hook is called when the page is refreshed why doesn't it set the data properly as it does when 'hot reloading'?
I have a minimal setup created with vue-cli, no fancy configurations whatsoever. Any clue what I might be missing?
I guess the employees are loaded async, right?
I don't know your exact application structure, but the problem is usually the following:
The mounted hook gets called, when the component mounts, of course. If the employees are loaded async in the parent component, the mount hook is called, before the async call is resolved. So it will copy an empty array at this time.
With a watcher you solve this problem, because the watcher fires as soon as the async call resolves (as it mutates the employees array).
Same happens to the hotreload. When the hotreload occurs, the mounted hook gets executed again - at this time the employees array is already prefilled with values and therefore the correct array is copied in the mount hook.
Update
If you want to avoid watchers, you could also wrap your component like this:
<your-component
v-if="employees.length > 0"
:employees="employees"
/>
Be aware, that the copied employees array IN your component is still not reactive. It just copies the array on the first time it has more than 1 value. A watcher really makes sense in this case.
If you use watchers, make sure to use the immediate: true option. This ensures, that the watcher is also called the first time on render (and also on hotreload).
Summary
If you really want to copy the array, use watchers (with the immediate: true flag).
If it's just about sorting, go for the computed property solution suggested by #Sebastian Scholl
It sounds like the component is Refreshing (reseting) with the prop change however it's not Re-mounting. This is what would cause the array to reset to it's default state ([]), whereas on hot-reload the actual page is reloading.
Try the following, and if it doesn't resolver the issue I would suggest going the route of using a Watcher.
<template>
<div>
sorted: {{ sortedEmployees }}
<br />
not sorted: {{ employees }}
</div>
</template>
<script>
export default {
props: {
employees: Array,
},
data() {
return {
sortedEmployees: Array.from(this.employees),
};
}
};
</script>
Another approach is to just use a Computed method so and add any filtering/sorting logic inside that method. It would be something like:
<template>
<div>
<input v-model="sortKey" />
sorted: {{ sortedEmployees }}
</div>
</template>
<script>
export default {
props: {
employees: Array,
},
data () {
return {
sortKey: ''
}
},
computed: {
sortedEmployees() {
return Array.from(this.employees).sort(this.sortingFunction);
},
},
methods: {
sortingFunction(a, b) {
// sorting function using this.sortKey
}
}
};
</script>
UPDATED ANSWER
I switched up the code in the example a little and believe to have gotten it to work as I you're describing.
App.js
First off, I made it so that the employees array is updated after 3 seconds.
<template>
<div id="app">
<Dashboard :employees="employees" />
</div>
</template>
<script>
import Dashboard from './components/Dashboard.vue';
export default {
name: 'App',
components: { Dashboard },
data () {
return {
employees: [
{
employeeId: '1',
firstName: 'Leite',
}
]
};
},
methods: {
updateEmployees () {
this.employees = this.employees.concat([
{
employeeId: '2',
firstName: 'Jacinto',
},
{
employeeId: '3',
firstName: 'Capelo',
}
]);
}
},
mounted () {
setTimeout(this.updateEmployees, 3000)
},
};
</script>
Dashboard.js
The updated() lifecycle hook runs whenever data changes are detected (props and data properties). This effectively detected the change in the prop passed by the parent App.js component and re-rendered the data - whereas the mounted hook only ran once per page load.
<template>
<div id="dashbord">
<div
v-for="(employee, index) in sortedEmployees"
:key="employee.employeeId"
>
{{ employee.firstName }}
</div>
</div>
</template>
<script>
export default {
name: 'Dashboard',
props: {
employees: Array
},
data() {
return {
sortedEmployees: Array.from(this.employees)
};
},
updated() {
this.sortedEmployees = Array.from(this.employees)
}
};
</script>

Vue 2 child and parent two way sync

this is my custom wrapper around vue-multiselect package. it is the child in this question.
<template>
<div>
<multiselect
v-model="items"
:options="filteredList"
:multiple="multiple"
:close-on-select="multiple ? false : true"
:show-labels="false"
:placeholder="placeholder"
track-by="id"
:label="label"
#input="inputChanged"
:internal-search="false"
#search-change="searchItems"
>
</multiselect>
</div>
</template>
<script>
export default {
model: {
prop: 'parentItems',
event: 'change',
},
props: ['multiple', 'list', 'placeholder', 'label', 'parentItems'],
data() {
return {
items: this.parentItems,
filteredList: this.list,
}
},
methods: {
searchItems(query) {
let q = latinize(query.toLowerCase().replace(/\s/g,''))
this.filteredList = this.list.filter(li => latinize(li[this.label].toLowerCase().replace(/\s/g,'')).includes(q))
},
inputChanged() {
this.$emit('change', this.items)
this.$emit('filters-changed')
},
resetItems() {
this.items = this.multiple ? [] : null
},
},
}
</script>
parent element looks like this
<custom-multiselect
v-model="car"
:multiple="false"
:list="cars"
placeholder="Car"
label="pseudo_id_name"
></custom-multiselect>
I either want to not use items, I'd rather only use parentItems, which of course, results in this warning
[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: "parentItems"
what counts as parent component re-render? in all my tests, it solved the two way sync and did not break anything. if using the prop as data was viable in my case, is there something i can do to suppress this warning?
if it is still wrong, i'd like to know how to make items synchronized. i read many stackoverflow questions/answers on this topic, but still don't fully undestand.
now with my v-model i $emit changes to the parent, which work just fine. i struggle with the other way around.
when on my parent element i call resetForm(), which sets this.car = null, child does not know that items should also become null. how do i listen for this event?
You could make items a computed prop with a getter and setter method, and bind it to your child component's model with v-model="items". This will work well as long as parentItems is a required prop, which means it is always passed to your custom component. If parentItems would be an optional prop I'd stick with your solution of keeping a copy of that prop in data (and coming up with a good default value), and emitting the change event in a watcher.
computed: {
items:{
get(){
return this.parentItems;
},
set(value){
this.$emit('change', value);
}
}
}
adding this to the child element seems to work. however, if there is a cleaner way, please let me know.
watch: {
parentItems: function (val, oldVal) {
this.items = this.parentItems
}
},

Throttle or debounce async calls in Vue 2 while passing arguments to debounced function

I have a Vue 2 application that uses an array of objects to back a search/multiselect widget provided by vue-multiselect.
I have looked at the Vue 1 -> 2 migration guide on debouncing calls, but the example they give did not propagate the arguments from the DOM elements to the business logic.
Right now the select fires change events with every keystroke, but I would like to throttle this (EG with lodash#throttle) so I'm not hitting my API every few milliseconds while they're typing.
import {mapGetters} from 'vuex';
import { throttle } from 'lodash';
import Multiselect from 'vue-multiselect'
export default {
components: {
Multiselect
},
data() {
return {
selectedWork: {},
works: [],
isLoading: false
}
},
computed: {
...mapGetters(['worksList']),
},
methods: {
getWorksAsync: throttle((term) => {
// the plan is to replace this with an API call
this.works = this.worksList.filter(work => titleMatches(work, term));
}, 200)
}
}
Problem: when the user types in the select box, I get the error:
TypeError: Cannot read property 'filter' of undefined
which is happening because this.worksList is undefined inside the throttle function.
Curiously, when I use the dev tools debugger, this.worksList has the value I need to dereference, with this referring to the Vue component.
Currently I am not calling the API from within the component, but the problem remains the same:
How can I throttle this call, and have the proper this context to update my this.works list? EDIT: this is explained in Vue Watch doesnt Get triggered when using axios
I also need to capture the user's query string from the multiselect widget to pass to the API call.
What is the proper pattern in Vue 2?
I ran into the same issue when using lodash.debounce. I'm a huge fan of arrow syntax, but I discovered that it was causing _.throttle() and _.debounce(), etc. to fail.
Obviously my code differs from yours, but I have done the following and it works:
export default {
...,
methods: {
onClick: _.debounce(function() {
this.$emit('activate', this.item)
}, 500)
}
}
Even though I'm not using arrow syntax here, this still references the component inside the debounced function.
In your code, it'd look like this:
export default {
...,
methods: {
getWorksAsync: throttle(function(term) {
// the plan is to replace this with an API call
this.works = this.worksList.filter(work => titleMatches(work, term));
}, 200)
}
}
Hope that helps!
I was unable to find an answer on SO (or anywhere) for this, but I eventually cobbled it together through trial and error, and from related materials here and in the docs.
Things that work that I didn't do, and why
It is possible to get get the value directly using a JavaScript DOM query, and it is also possible to dig in to the multiselect component's structure and get the value. The first solution circumvents the framework, the second depends on undocumented attributes of the multiselect component. I am avoiding both of those solutions as non-idiomatic and brittle.
My current solution
Updated an attribute on the component whenever there was a change event in the search box. This allowed me to capture the user's query string.
Called my throttled async function from inside the event listener.
Passed a regular function instead of an arrow function to throttle, which gave the correct this (the Vue component.)
If anyone has a suggestion for a better way to do this in Vue 2, I'm all ears.
Here's what my solution looked like in the end:
<template>
<div>
<label
class="typo__label"
for="ajax">Async select</label>
<multiselect
id="ajax"
v-model="selectedWork"
label="title"
track-by="id"
placeholder="Type to search"
:options="works"
:searchable="true"
:loading="isLoading"
:internal-search="false"
:multiple="false"
:clear-on-select="true"
:close-on-select="true"
:options-limit="300"
:limit="3"
:limit-text="limitText"
:max-height="600"
:show-no-results="false"
open-direction="bottom"
#select="redirect"
#search-change="updateSearchTerm">
<span slot="noResult">Oops! No elements found. Consider changing the search query.</span>
</multiselect>
</div>
</template>
<script>
import {mapGetters} from 'vuex';
import { throttle } from 'lodash';
import Multiselect from 'vue-multiselect'
export default {
components: {
Multiselect
},
data() {
return {
searchTerm: '',
selectedWork: {},
works: [],
isLoading: false
}
},
computed: {
...mapGetters(['worksList']),
},
methods: {
limitText(count) {
return `and ${count} other works`;
},
redirect(work) {
// redirect to selected page
},
updateSearchTerm(term){
this.searchTerm = term;
this.isLoading = true;
this.getWorksAsync();
},
getWorksAsync: throttle(function() {
const term = this.searchTerm.toLowerCase();
callMyAPI(term)
.then(results => {
this.works = results;
this.isLoading = false;
})
}, 200)
}
}
</script>

Vue 2 - Mutating props vue-warn

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.

Categories