Vue2: warning: Avoid mutating a prop directly - javascript

I'm stuck in the situation where my child component (autocomplete) needs to update a value of its parent (Curve), And the parent needs to update the one of the child (or to completely re-render when a new Curve component is used)
In my app the user can select a Curve in a list of Curve components. My previous code worked correctly except the component autocomplete was not updated when the user selected another Curve in the list (the component didn't update its values with the value of the parent).
This problem is fixed now but I get this warning:
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:
"value"
The description of this warning explain exactly what behavior I expect from my components. Despite this warning, this code works perfectly fine !
Here is the code (parts of it have been removed to simplify)
// curve.vue
<template>
<autocomplete v-model="curve.y"></autocomplete>
</template>
<script>
import Autocomplete from './autocomplete'
export default {
name: 'Curve',
props: {
value: Object
},
computed: {
curve() { return this.value }
},
components: { Autocomplete }
}
</script>
// autocomplete.vue
<template>
<input type="text" v-model="content"/>
</template>
<script>
export default {
name: 'Autocomplete',
props: {
value: {
type: String,
required: true
}
},
computed: {
content: {
get() { return this.value },
set(newValue) { this.value = newValue }
}
}
}
</script>
A lot of people are getting the same warning, I tried some solutions I found but I was not able to make them work in my situation (Using events, changing the type of the props of Autocomplete to be an Object, using an other computed value, ...)
Is there a simple solution to solve this problem ? Should I simply ignore this warning ?

you can try is code, follow the prop -> local data -> $emit local data to prop flow in every component and component wrapper.
ps: $emit('input', ...) is update for the value(in props) bind by v-model
// curve.vue
<template>
<autocomplete v-model="curve.y"></autocomplete>
</template>
<script>
import Autocomplete from './autocomplete'
export default {
name: 'Curve',
props: {
value: Object
},
data() {
return { currentValue: this.value }
}
computed: {
curve() { return this.currentValue }
},
watch: {
'curve.y'(val) {
this.$emit('input', this.currentValue);
}
},
components: { Autocomplete }
}
</script>
// autocomplete.vue
<template>
<input type="text" v-model="content"/>
</template>
<script>
export default {
name: 'Autocomplete',
props: {
value: {
type: String,
required: true
}
},
data() {
return { currentValue: this.value };
},
computed: {
content: {
get() { return this.value },
set(newValue) {
this.currentValue = newValue;
this.$emit('input', this.currentValue);
}
}
}
}
</script>

You can ignore it and everything will work just fine, but it's a bad practice, that's what vue is telling you. It'll be much harder to debug code, when you're not following the single responsibility principle.
Vue suggests you, that only the component who owns the data should be able to modify it.
Not sure why events solution ($emit) does not work in your situation, it throws errors or what?
To get rid of this warning you also can use .sync modifier:
https://v2.vuejs.org/v2/guide/components.html#sync-Modifier

Related

Relationship between props and data (vue)

In
https://codesandbox.io/s/v9pp6
the ChromePage component passes a prop to InventorySectionC:
<inventory-section-component :itemSectionProps="getItemSection">
</inventory-section-component>
InventorySectionC:
<template>
<div class="inventory-section-component">
<draggable v-model="itemSectionProps.itemSectionCategory">
<transition-group>
<div
v-for="category in itemSectionProps.itemSectionCategory"
:key="category.itemSectionCategoryId"
>
<!-- <p>{{ category.itemSectionCategoryName }}</p> -->
<inventory-section-group-component :itemSectionGroupData="category">
</inventory-section-group-component>
</div>
</transition-group>
</draggable>
</div>
</template>
<script>
import InventorySectionGroupComponent from "./InventorySectionGroupC";
import draggable from "vuedraggable";
export default {
name: "InventorySectionComponent",
components: {
InventorySectionGroupComponent,
draggable,
// GridLayout: VueGridLayout.GridLayout,
// GridItem: VueGridLayout.GridItem,
},
props: {
itemSectionProps: {
type: Object,
},
},
data() {
let itemSectionData = itemSectionProps;
return {
itemSectionData
};
},
};
</script>
<style scoped>
</style>
gives a warning at line:
<draggable v-model="itemSectionProps.itemSectionCategory">
:
Unexpected mutation of "itemSectionProps" prop. (vue/no-mutating-props)eslint
Why (how?) is itemSectionProps mutable?
Can a binding be created between props and data (all draggable samples use a data object:
https://sortablejs.github.io/Vue.Draggable/#/nested-example
https://github.com/SortableJS/Vue.Draggable/blob/master/example/components/nested-example.vue
)?
The idea is to have auto updating, nested, draggable components.
The code as is "works" but there are warnings/errs:
data() can't seem to see props:
And one more thing, which comes "first"? Data or props? can't seem to figure it out from the docs:
https://v2.vuejs.org/v2/guide/instance.html
Setting the props to a predefined value:
props: {
itemSectionProps: {
type: Object,
default: { itemSectionCategory: '' }
},
},
gives:
Type of the default value for 'itemSectionProps' prop must be a function. (vue/require-valid-default-prop).
I'm not sure why vue expects props to return a function.
After adding a default() onto props, props are empty when passed on to components:
https://codesandbox.io/s/sjm0x
(this grew too long for a comment, but probably already answers what you need)
itemSectionProps:
Your props are defined as:
props: {
itemSectionProps: {
type: Object,
},
},
You reference a prop of that object in your template
<draggable v-model="itemSectionProps.itemSectionCategory">
Vue cannot assume itemSectionProps.itemSectionCategory will exist in the future.
You should give it a default (see Vue docs) to create the expected values in that object.
props: {
itemSectionProps: {
type: Object,
default() {
return { itemSectionCategory: '' };
}
},
},
Do this for all the props you use on itemSectionProps.
data() can't seem to see props:
You can write this.itemSectionProps instead of only itemSectionProps.
But itemSectionProps is already defined in props. You can just remove itemSectionProps from data.
If you need to change that value, use a copy and promote changes with this.$emit.
You are probably calling the props without using this. on your data method.
You can as well define your variable itemSectionData as below:
data(){
return {
itemSectionData: Object.assign({}, this.itemSectionProps)
}
}
Object.assign()
The Object.assign() method copies all enumerable own properties from one or more source objects to a target object. It returns the target object. See more details here
Then use the newly defined variable itemSectionData within your component. Like:
<draggable v-model="itemSectionData.itemSectionCategory">
If you want to update the prop's values, simply emit an event from your child component and capture it on the parent as below:
methods:{
updatePropValues(){
this.$emit('updateProp', this.yourNewValues);
}
}
On the parent component handle the event as:
<inventory-section-component #updateProp="setNewValues" :itemSectionProps="getItemSection">
</inventory-section-component>
methods:{
setNewValues(newValues){
this.itemSections = newValues;
}
}
Check it out in action here

How to get the props value and use in lifehooks cycle of Vue JS

I'm searching a way to get the props value through some lifehooks like mounted or updated and trying to save the value with my v-model with some string. But I can't get it.
Though I tried :value on the input element with the props value and some string and I was able to get it, but it seems like I can't access it without v-model, as I researched v-model and :value can't be together.
The purpose is to get the value(with from props and some string) of a input tags.
Parent Component
<invite :user_token="user_token"/>
Child Component
export default {
props: ['user_token'],
data() {
return {
link: ''
}
},
mounted() {
console.log(this.user_token);
this.link = `"http://localhost/johndoe-vue/public/#/invite${this.user_token}"`;
},
updated() {
console.log(this.user_token);
this.link = `"http://localhost/johndoe-vue/public/#/invite${this.user_token}"`;
}
}
Welcome to SO Nigel!
Are you looking for something like this, perhaps?
ParentComponent.vue
<template>
<div id="wrapper">
<invite :userToken="userToken"></invite>
</div>
</div>
<script>
import Invite from "#/Invite.vue";
export default {
components: {
Invite
},
data() {
return {
userToken: "fooBar",
};
}
}
</script>
ChildComponent.vue
<template>
<div id="wrapper">
<p v-if="inviteLink != ''">{{ inviteLink }}</p>
</div>
</template>
export default {
props: {
userToken: {
type: String,
}
},
data() {
return {
inviteLink: ""
}
},
created() {
if(this.userToken != "") {
this.inviteLink == "/your-link-here/"+this.userToken;
}
}
}
Also, you should check out the Vue.js Style Guide. They've marked multi-word component names as essential. Your Invite component should be renamed to BaseInvite or something like that.
Have you tried to $emit this.link
Props is accessible through the $props property of your component. You would reference it like: this.$props.[property name]. $props is called an instance property; there are many of them and they are each accessible this way. See https://v2.vuejs.org/v2/api/#Instance-Properties
Keep in mind that the Vue life cycle methods are somewhat inconsistent. Which instance properties are accessible depends on the method (ie: you can't reference $el in created(...).
https://v2.vuejs.org/v2/guide/instance.html#Lifecycle-Diagram

Wrapping a JS library in a Vue Component

I need to know which is best practice for wrapping a JS DOM library in Vue. As an example I will use EditorJS.
<template>
<div :id="holder"></div>
</template>
import Editor from '#editorjs/editorjs'
export default {
name: "editorjs-wrapper",
props: {
holder: {
type: String,
default: () => {return "vue-editor-js"},
required: true
}
},
data(){
return {
editor: null
};
},
methods: {
init_editor(){
this.editor = new Editor({
holder: this.holder,
tools: {
}
});
}
},
mounted(){
this.init_editor();
},
//... Destroy on destroy
}
First:
Supose I have multiple instances of <editorjs-wrapper> in the same View without a :hook then all intances would have the same id.
<div id="app">
<editorjs-wrapper></editorjs-wrapper>
<editorjs-wrapper></editorjs-wrapper>
</div>
Things get little weird since they both try to process the DOM #vue-editor-js. Would it be better if the component generated a random id as default?
Second:
EditorJS provides a save() method for retrieving its content. Which is better for the parent to be able to call save() method from the EditorJS inside the child?
I have though of two ways:
$emit and watch (Events)
// Parent
<div id="#app">
<editorjs-wrapper #save="save_method" :save="save">
</div>
// Child
...
watch: {
save: {
immediate: true,
handler(new_val, old_val){
if(new_val) this.editor.save().then(save=>this.$emit('save', save)) // By the way I have not tested it it might be that `this` scope is incorrect...
}
}
}
...
That is, the parent triggers save in the child an thus the child emits the save event after calling the method from the EditorJS.
this.$refs.childREF
This way would introduce tight coupling between parent and child components.
Third:
If I want to update the content of the child as parent I don't know how to do it, in other projects I have tried without success with v-modal for two way binding:
export default{
name: example,
props:{
content: String
},
watch:{
content: {
handler(new_val, old_val){
this.update_content(new_val);
}
}
},
data(){
return {
some_js_framework: // Initialized at mount
}
},
methods: {
update_content: function(new_val){
this.some_js_framework.update(new_val)
},
update_parent: function(new_val){
this.$emit('input', this.some_js_framework.get_content());
}
},
mounted(){
this.some_js_framework = new Some_js_framework();
this.onchange(this.update_parent);
}
}
The problem is:
Child content updated
Child emit input event
Parent updates the two-way bidding of v-model
Since the parent updated the value the child watch updates and thus onchange handler is triggered an thus 1. again.

Vuex : state change not updating input field

**** UPDATE ****
Solution : Not bothering to tie the Vuex state to the form. I realized it's not worth the trouble, and I can still serialize the form data and send it along anyway using the standard conventions. Had a total rethinking of not needing to tie so many elements into JS objects.
**** ORIG ****
Alright, can someone please tell me what I'm missing here.
I want to update the state in a mutation function, and have the changes reflect in the input fields. Crazy right? I guess there's trouble with updating object properties, so in the example I have both referencing the object property and defining a specific getter for the property, neither of which are working. Then to represent updating the state outside of the input field, I just have a button below that does this.
Template:
<input :value="user.first_name" #input="UPDATE_USER" />
OR
<input :value="first_name" #input="UPDATE_USER" />
<button v-on:click="updateName">Update</button>
Component:
computed: {
...mapState({
user: state => state.user,
first_name: state => state.user.first_name // specific getter for the property
})
},
methods: {
...mapActions([
UPDATE_USER,
]),
updateName () {
this.$store.dispatch(UPDATE_USER_NAME, "anything");
}
}
Action:
[UPDATE_USER_NAME] ({commit}, name) {
commit("updateUserName", name);
},
// Omitted UPDATE_USER function, just takes e.target.name / value and it works anyway
Mutations:
updateUserName (state, name) {
Vue.set(state.user, "first_name", name);
}
Expected: Click the button, it updates the state, and the input field value shows "anything".
Actual: Click the button, it updates the state, but the input field value is not updated. This goes for both input fields, one which references the object property directly, and the other which has its own getter.
Editing the input fields works fine, so it's like it works top-down but not bottom-up. What the heck am I missing?
Note: I tested this locally but I don't know exactly how the store state looks from the question.
The <input> can be two-way bound with v-model, instead of computed or watched. It can be initialized with a value from the store (I used the mounted lifecycle hook below). The store state is updated only on button click.
<input v-model="firstName"/>
<button #click="updateName">Update</button>
import { mapActions, mapGetters } from 'vuex'
export default {
data () {
return {
firstName: ''
}
},
computed: mapGetters(['getFirstName']),
methods: {
...mapActions(['UPDATE_FIRST_NAME']), // get the action from the store
updateName () {
// validations can go here etc
this.UPDATE_FIRST_NAME(this.firstName) // update vuex store state
}
},
mounted () {
this.firstName = this.getFirstName // initialize this.firstName -> <input>
}
}
With this solution, you'd have to make sure to create the getter in your store, as in the following example:
const state = {
user: {
firstName: ''
}
}
const getters = {
getFirstName: state => state.user.firstName
}
You need to watch the state variables. When it change the value, the the watch function will fire.
export default {
data() {
return {
dummy_name: '',
first_name: '',
}
},
created(){
},
computed: {
dummy_name() {
return this.$store.state.user.first_name
},
watch: {
dummy_name() {
this.first_name = this.dummy_name
}
}
Hope this will help, and get some idea how watch and computed work.
This is an old question, but I ran into the same problem yesterday and the solution was to use the .prop modifier on the input value attribute like so:
<input :value.prop="first_name" #change="updateName" />
A quote from quite random place in the docs says:
For some properties such as value to work as you would expect, you will need to bind them using the .prop modifier.
Hope this helps someone!

Setting props for components using v-for in Vue JS

I have a PhoneCard.vue component that I'm trying to pass props to.
<template>
<div class='phone-number-card'>
<div class='number-card-header'>
<h4 class="number-card-header-text">{{ cardData.phone_number }}</h4>
<span class="number-card-subheader">
{{ cardData.username }}
</span>
</div>
</div>
</template>
<script>
export default {
props: ['userData'],
components: {
},
data() {
return {
cardData: {}
}
},
methods: {
setCardData() {
this.cardData = this.userData;
console.log(this.cardData);
}
},
watch: {
userData() {
this.setCardData();
}
}
}
The component receives a property of userData, which is then being set to the cardData property of the component.
I have another Vue.js component that I'm using as a page. On this page I'm making an AJAX call to an api to get a list of numbers and users.
import PhoneCard from './../../global/PhoneCard.vue';
export default {
components: {
'phone-card': PhoneCard
},
data() {
return {
phoneNumbers: [],
}
},
methods: {
fetchActiveNumbers() {
console.log('fetch active num');
axios.get('/api').then(res => {
this.phoneNumbers = res.data;
}).catch(err => {
console.log(err.response.data);
})
}
},
mounted() {
this.fetchActiveNumbers();
}
}
Then once I've set the response data from the ajax call equal to the phoneNumbers property.
After this comes the issue, I try to iterate through each number in the phoneNumber array and bind the value for the current number being iterated through to the Card's component, like so:
<phone-card v-for="number in phoneNumbers" :user-data="number"></phone-card>
However this leads to errors in dev tools such as property username is undefined, error rendering component, cannot read property split of undefined.
I've tried other ways to do this but they all seem to cause the same error. any ideas on how to properly bind props of a component to the current iteration object of a vue-for loop?
Try
export default {
props: ['userData'],
data() {
return {
cardData: this.userData
}
}
}
Answered my own question, after some tinkering.
instead of calling a function to set the data in the watch function, all I had to do was this to get it working.
mounted() {
this.cardData = this.userData;
}
weird, I've used the watch method to listen for changes to the props of components before and it's worked flawlessly but I guess there's something different going on here. Any insight on what's different or why it works like this would be cool!

Categories

Resources