Why I shouldn't set data from computed? - javascript

Using vuex, I receive an object, lets suppose
user: {name: 'test'}
And in app.vue I use this.$store.getters.user
computed: {
user: function() {
let user = this.$store.getters.user
return user
}
}
While setting also data object 'this.name'
data() {
return {
name: ''
}
}
computed: {
user: function() {
let user = this.$store.getters.user
this.name = user.name
return user
}
}
But in the lint I get this error 'unexpected side effect in computed property', (the data 'name' should be used as a v-model, to be used as a update API parameter).
I know it can be ignored if you know what you're doing, and that it is triggered for setting data from computed, but why it triggers this error? and how to workaround it?

don't set value in computed. if you need to get name of computed user you must be create new computed:
user: function() {
let user = this.$store.getters.user
return user
},
name: function() {
if(this.user.name!=undefined) return this.user.name
return ''
},
and remove name from data
but if you realy need to set name you can watch user and set name
watch: {
user(newVal) {
if(newVal.name!=undefined) this.name = newVal.name
}
}

Vue has both computed getters and setters. If you define a computed property as you did above it is only a getter. A getter is meant to only "get" a value and it should be a "pure" function with no side effects so that it is easier to read, debug and test.
From the docs for the rule that triggered the linting error on your code:
It is considered a very bad practice to introduce side effects inside
computed properties. It makes the code not predictable and hard to
understand.
In your case you might want to use a computed setter for either the user or the name values so that you can use them as v-models. You could, for example, do:
computed: {
user: function () {
return this.$store.getters.user;
},
user: {
// getter
get: function () {
return this.user.name;
},
// setter
set: function (newValue) {
this.$store.commit('setUserName', newValue);
}
}
}

Related

Why won't my template update with it being bound to a computed property?

I am facing an issue where I have some template HTML in a component that relies on the computed getter of a Vuex method. As you can see in the template, I am simply trying to show the output of the computed property in a <p> tag with {{ getNumSets }}.
As I update the state with the UPDATE_EXERCISE_SETS mutation, I can see in the Vue devtools that the state is updated correctly, but the change is not reflected in the <p> {{ getNumSets }} </p> portion.
Template HTML:
<template>
...
<v-text-field
v-model="getNumSets"
placeholder="S"
type="number"
outlined
dense
></v-text-field>
<p>{{ getNumSets }}</p>
...
</template>
Component Logic:
<script>
...
computed: {
getNumSets: {
get() {
var numSets = this.$store.getters['designer/getNumSetsForExercise']({id: this.id, parent: this.parent})
return numSets
},
set(value) { // This correctly updates the state as seen in the Vue DevTools
this.$store.commit('designer/UPDATE_EXERCISE_SETS', {
id: this.exerciseId,
parentName: this.parent,
numSets: parseInt(value),
date: this.date
})
}
}
...
</script>
Vuex Store Logic:
...
state: {
designerBucket: []
},
getters: {
getNumSetsForExercise: (state) => (payload) => {
var numSets = 0
for (var i = 0; i < state.designerBucket.length; i++) {
if (state.designerBucket[i].id == payload.id) {
numSets = state.designerBucket[i].numSets
}
}
return numSets
}
},
mutations: {
UPDATE_EXERCISE_SETS(state, payload) {
state.designerBucket.forEach(exercise => {
if (exercise.id == payload.id) {
exercise.numSets = payload.numSets
}
})
}
}
Any insight is very appreciated!
P.S. I have also tried using a for (var i=0...) loop, looping over the indices and then using Vue.set() to set the value. This did update the value in the store as well, but the computed property is still not updating the template.
This turned into a bit of a long-winded answer, but bear with me.
Here's my hunch: since you're returning a function from your Vuex getter, Vue isn't updating your computed property on state changes because the function never changes, even if the value returned from it would. This is foiling the caching mechanism for computed properties.
Reactivity for Arrow Function Getters
One of the things to keep in mind when creating a getter like this, where you return an arrow function:
getNumSetsForExercise: (state) => (payload) => {
var numSets = 0
for (var i = 0; i < state.designerBucket.length; i++) {
if (state.designerBucket[i].id == payload.id) {
numSets = state.designerBucket[i].numSets
}
}
return numSets
}
...is that you're no longer returning actual state data from your getter.
This is great when you're using it to pull something from state that depends on data that's local to your component, because we don't need Vue to detect a change, we just need the function to access current state, which it does fine.
BUT, it may also lead to the trap of thinking that updating state should update the getter, when it actually doesn't. This is really only important when we try to use this getter in a computed property like you have in the example, due to how computed properties track their dependencies and cache data.
Computed Caching and Dependency Detection
In Vue, computed properties are smarter than they first seem. They cache their results, and they register and track the reactive values they depend on to know when to invalidate that cache.
As soon as Vue calculates the value of a computed property, it stores it internally, so that if you call the property again without changing dependencies, the property can return the cached value instead of recalculating.
The key here for your case is the dependency detection– your getter has three dependencies that Vue detects:
get() {
var numSets = this.$store.getters['designer/getNumSetsForExercise']({id: this.id, parent: this.parent})
return numSets
},
The getter: this.$store.getters['designer/getNumSetsForExercise']
this.id
this.parent
None of these values change when <v-text-field> calls your setter.
This means that Vue isn't detecting any dependency changes, and it's returning the cached data instead of recalculating.
How to Fix it?
Usually, when you run into these sorts of dependency issues, it's because the design of the state could be improved, whether by moving more data into state, or by restructuring it in some way.
In this case, unless you absolutely need designerBucket to be an array for ordering purposes, I'd suggest making it an object instead, where each set is stored by id. This would simplify the implementation by removing loops, and remove the need for your getter altogether:
...
state: {
designerBucket: {}
},
mutations: {
UPDATE_EXERCISE_SETS(state, payload) {
// Need to use $set since we're adding a new property to the object
Vue.set(state.designerBucket, payload.id, payload.numSets);
}
}
Now, instead of invoking a getter, just pull designerBucket from state and access by this.id directly:
<script>
...
computed: {
getNumSets: {
get() {
return this.$store.state.designerBucket[this.id];
},
set(value) {
this.$store.commit('designer/UPDATE_EXERCISE_SETS', {
id: this.exerciseId,
parentName: this.parent,
numSets: parseInt(value),
date: this.date
});
}
}
...
</script>
This should allow Vue to detect changes correctly now, and prevent the stale cache problem from before.
Edited: First import mapGetters from 'vuex' like this on the top of the script tag.
import { mapGetters } from "vuex"
Now in your computed object, add mapGetters and pass arguments to the getter method inside the get() method like this:-
computed: {
...mapGetters('designer',['getNumSetsForExercise']),
getNumSets: {
get() {
var numSets = this.getNumSetsForExercise({id: this.id, parent: this.parent})
return numSets
},
set(value) { // This correctly updates the state as seen in the Vue DevTools
this.$store.commit('designer/UPDATE_EXERCISE_SETS', {
id: this.exerciseId,
parentName: this.parent,
numSets: parseInt(value),
date: this.date
})
}
}
And see if it works.

Vue Computed Property not updating. Very strange behaviour

Yes, it's another 'Vue computed property is not updating question...
Below is an excerpt of my component with the issue. I have a computed property 'fieldModel' this uses Vue.set to set a new value, then i console log that computed property immediately after assigning it a new value the javascript object updates and is viewable in devtools, the computed property however has not updated, and neither has the DOM.
export default {
props:{
value:{
type:Object,
required:true,
}
},
data() {
return {
model:this.value,
key:'something',
}
},
created() {
var self = this;
setTimeout(function() {
self.fieldModel = 'Apples';
}, 1000);
},
computed:{
fieldModel:{
get() {
return this.model[this.key];
},
set(value) {
var self = this;
self.$set(self.model, self.key, value);
console.log(self.model[self.key], self.fieldModel);
//Logs out 'Apples', undefined,
}
}
}
}
The example code i posted in the original question works correctly, This lead me to break down my code and resolve my issue.
I had this component in a v-for loop with recursive nesting, Another component appeared to mutate the v-model object without updating these components resulting in some very strange behaviour.
I was able to solve the problem by adding a watcher for 'value' to update the model field and a watcher for 'model' to $emit('input') any changes to the model to it's parent.
This results in an infinite loop that crashes the browser, however i was able to resolve that by adding a check to see if the model/value is the same object
Example code is simplified for brevity:
{
watch:{
value(newValue) {
if(this.model != newValue) {
this.model = newValue;
}
},
model(newModel) {
this.$emit('input', newModel)
},
}
}

Vue this inside data() factory function

Can I rely on this used inside data factory function as it was current component object instance? I couldn't find in docs what this is in data().
data() {
return {
results: [],
apiResource: this.$resource('url...'), // <-- this used here
loading: true,
}
},
Simple test shows that this is VueComponent instance here, but the question is if the framework allows using it this way.
Yes, you can rely on this in the data factory function pointing to the component, depending on how you define the function. It's the primary way of initializing local data with values from properties, for example.
props:["value"],
data(){
return {
localValue: this.value
}
}
If, however, you defined your data function with an arrow function, this will not be the component.
props:["value"],
data: () => {
// 'this' is NOT the component
return {
localValue: this.value // results in undefined
}
}
I think no
Perhaps you need
data() {
return {
results: [],
set apiResource(v){},
get apiResource()( return this.$resource('url...')), // <-- this used here
loading: true,
}
},

Unable to access props values in data method in vuejs

I have the code (vuejs2) -
Vue.component('competetion-list', {
template: `<div>{{totalCompetetions}}</div>`,
props: ['values'],
data: function () {
return { totalCompetetions: this.values.length}
}
})
Nothing is printed on the page but if I change the template value to
template: `<div>{{this.values.length}}</div>`
it prints 15. What am I doing wrong and how can I pass the props to the data?
Any help is much appreciated.
I was unable to assign the prop values to data totalCompetetions in the following way -
data: function () {
return { totalCompetetions: this.values.length}
}
But I was able to do it using the watch, computed, and methods properties.
With watch property -
watch: {
values: function(){
this.totalCompetitions = this.values;
}
}
With computed property -
computed:{
competition:{
get: function(){
return this.values.length;
}
}
With methods property -
methods:{
competitionn: function(){
return this.values.length;
}
}
But for computed and methods properties, I needed to set totalCompetetions in the following way -
For computed -
template: `<div><p>{{totalCompetitions = competition}}</p></div>` //without parenthesis
For methods -
template: `<div><p>{{totalCompetitions = competition()}}</p></div>` //with parenthesis
You code does work.
I guess the problem is your parent component. Did you pass the values correctly? for example:
<competetion-list :values="[1, 2, 3]"></competetion-list>
Besides, for your case I'd say computed properties is a better solution.
computed: {
totalCompetetions () {
return this.values.length
}
}
From the data() method, you should be able to reference the component's properties using this.
Try following:
Vue.component('competetion-list', {
template: `<div>{{totalCompetetions}}</div>`,
props: ['values'],
data: function () {
var data = { totalCompetetions: this.values.length}
return data
}
})
As validly mentioned in the comment, if values array is changing later, you may have to put a watcher on the prop and inside watcher, set totalCompetetions as this.values.length.

Polymer readOnly access private setter for given property

I am aware you can set a readOnly property by using the generated setter: _setMyProp(newValue)
But what if I don't know the name of the property before-hand? Is there any way to access the setter for a given property? My current solution is mapping property names to their setter functions:
_setters: {
type: Object,
value: {
prop1 : function(newVal) { this._setProp1(newVal); },
prop2 : function(newVal) { this._setProp2(newVal); }
}
}
Is there a more elegant way to do this?
Update
For more context, this is the function that calls _setters. This function should ideally not use any actual property names. This is what I meant by "don't know the name of the property before-hand"
load: function(data, key) {
if (!_.isEmpty(data)) {
if (key) {
this._setters[key].bind(this)(data);
} else {
_.each(this._setters, function(setter, key) {
setter.bind(this)(data[key]);
}.bind(this));
}
}
},
The element behaves like iron-meta and loads data from a global object when it's ready, and sets all it's properties.
ready: function() {
this.load(_data);
},
But it must also load specific properties when an ajax response comes in, for each attached element.
_els.forEach(function(el) {
el.load(key, _data[key]);
});
I'm trying to make it as easy as possible to define more properties later on. Polymer's this.set('propName', 'value'); would be perfect but does not work for readOnly properties.
If I should just scrap this entirely and go a different route, I'd also be open to that advice.
Update 2
My solution for lack of a better one was to abandon readOnly and just use this.set
This is what load looks like now:
load: function(data, property) {
if (!_.isEmpty(data)) {
if (property) {
this.set(property, data[property]);
} else {
Object.keys(this.properties).forEach(function(property) {
this.set(property, data[property]);
}.bind(this));
}
}
},

Categories

Resources