Vue computed issue - when will it compute again - javascript

Vue computed has already perplexed me for a while
when will it compute again
condition1:
data() {
return {
cart:{
item:{
nums: 10,
price: 10
}
}
};
},
computed: {
total() {
return this.cart.item.nums * this.cart.item.price
}
},
methods:{
set(){
this.cart.item = {
nums: 5,
price: 5
}
}
}
computed will work
condition2:
data() {
return {
cart: [{
nums: 10,
price: 10
}]
};
},
computed: {
total() {
return this.cart[0].nums * this.cart[0].price
}
},
methods:{
set(){
this.cart[0] = {
nums: 5,
price: 5
}
}
}
computed won't work
I know this is the solution, but why?
methods:{
set(){
this.cart[0].nums = 5
this.cart[0].price = 5
}
}
}
why didn't it be observed in condition2 ?
why Vue don't want it be observed ?

Reactivity with objects and arrays is a bit finicky with Vue. With other variables it is easy to detect when they are changed, but with objects and arrays it is not always possible to detect whenever something in the object/array has been changed. (That is, without Proxies, which will come in Vue 3.x)
In your case, total will be recalculated if this.cart is marked as changed, this.cart[0] is marked as changed or if this.cart[0].nums or this.cart[0].price is changed. The problem is that you are replacing the object in this.cart[0]. This means that this.cart[0].price and nums do not change, because those still point to the old object. Apparently, this.cart[0] and this.cart are not marked as changed, so Vue still believes total to be up-to-date.
There are several ways to get around this. One is to use Vue's helper methods to work with objects/arrays, namely Vue.set, Vue.delete. You can access them in your SFC with this.$set or this.$delete. As this.$set explicitly marks whatever you pass as first argument as "changed", your total will also be updated.
this.$set(this.cart, 0, {
nums: 2,
price: 100
});
Another way is to modify the object itself, rather than replacing it. Since you are still working with the same object, Vue will detect that this.cart[0] has changed.
setItem() {
this.cart[0] = Object.assign(
this.cart[0],
{
nums: 5,
price: 5
}
);
}
Another way is to use one of the many array methods. In your case, you could use Array.prototype.splice. As this is a function call, Vue can detect that the function is called and can mark the correct items as changed, which will trigger an update of anything that relies on it.
this.cart.splice(0, 1, {
nums: 50,
price: 10
});

Related

What is the difference between those two state updates in react?

I'm studying a React course at a point where the instructor is explaining about updating states and I cannot understand how those two snippets are really different internally, please see in the codepen links below:
Updates the state directly snippet
class Counters extends Component {
state = {
counters: [
{ id: 1, value: 4 },
{ id: 2, value: 0 },
{ id: 3, value: 0 },
{ id: 4, value: 0 }
]
};
handleIncrement = counter => {
const updatedCounters = [...this.state.counters];
updatedCounters[0].value++;
};
}
Updates the state indirectly then save snippet
class Counters extends Component {
state = {
counters: [
{ id: 1, value: 4 },
{ id: 2, value: 0 },
{ id: 3, value: 0 },
{ id: 4, value: 0 }
]
};
handleIncrement = counter => {
const updatedCounters = [...this.state.counters];
const index = updatedCounters.indexOf(counter);
updatedCounters[index] = {...counter};
updatedCounters[index].value++;
this.setState({ counters: updatedCounters });
};
}
In this lecture, the instructor explains that the first snippet does update the state directly.
So my question is, if the first example, as the instructor says, updates the state directly, the only thing that prevents the second snippet from updating the state directly is the line below?:
updatedCounters[index] = {...counter};
If that is true, how does it works?
In the first example, updatedCounters is a copy of this.state.counters, but the items inside that copy are references to the exact same objects in the original. This might be analogous to moving some books to a new box. The container changed, but the contents did not. In the second example, you don't mutate the selected counter, you copy the counter and then mutate that copy.
When it comes to state modifications in React, You always need to remember about the fact that the state can not be mutated. Well , technically You can do that but it's a bad practice and it's a antipattern. You always want to make a copy of the state and modify the copy instead of the original state object. Why ? It really improves the performance of the application and that's React's huge advantage. It's called immutability.
You might also ask .. "How does this approach improve the performance?"
Well, basically, thanks to immutability pattern, react does not have to check the entire state object. Instead of that, React does a simple reference comparison. If the old state isn't referencing the same obj. in memory -> we know that the state has changed.
Always try to use .setState() to avoid mutating state in a wrong way and that's what you do here:
handleIncrement = counter => {
const updatedCounters = [...this.state.counters];
updatedCounters[0].value++;
};
Basically the answer to my own question is that, if you do not call setState react wont trigger the necessary routines to update the Component "view" value on screen (mentioning the first example)

Returning arrow function from Vuex storage getter: Not understanding

I have been watching one Vuex course and up until now everything was fine till they included arrow function in a getter and then using it in computed property and action. The code is following:
item structure:
const _products = [
{ id: 1, title: "iPad 4 Mini", price: 500.01, inventory: 2 },
{ id: 2, title: "H&M T-Shirt White", price: 10.99, inventory: 10 },
{ id: 3, title: "Charli XCX - Sucker CD", price: 19.99, inventory: 5 }
];
a getter in store.js:
productIsInStock() {
return product => {
return product.inventory > 0;
};
}
an action in store.js which uses this getter:
addProductToCart(context, product) {
if (context.getters.productIsInStock(product)) {
let cartItem = context.state.cart.find(item => item.id === product.id);
if (!cartItem) {
context.commit("pushProductToCart", product.id);
} else {
context.commit("incrementItemQuantity", cartItem);
}
context.commit("decrementProductInventory", product);
}
},
a computed which uses this getter and template, ProductList.vue :
<template>
<li v-for="(product, index) in products" v-bind:key="index">
{{product.title}} - {{product.price | currency}} - {{product.inventory}}
<button
#click="addProductToCart(product)"
:disabled="!productIsInStock(product)"
>
Add product to cart
</button>
</li>
</template>
// ...
computed: {
products() {
return this.$store.state.products;
},
productIsInStock() {
return this.$store.getters.productIsInStock;
}
},
It is totally working but I don't understand why. Mainly I do not understand how this getter works inside both computed and if statement. I tried to repeat the same structure in the console but for some reason it didn't work at all. Hope I provided enough code
Let me see if I understand your doubt.
productIsInStock is a getter, that returns an arrow function, and vue evaluates this in the render function, through the template part:
<button
#click="addProductToCart(product)"
:disabled="!productIsInStock(product)">
Add product to cart
</button>
It's important first to understand that vue, thanks to :disable databind, evaluates the value as javascript. So it calulates the actual value of the getter (that is a function), and YOU call the actual return value (remember, is a function) that return something.
Put on another way: The getter are called everytime that related state changes, to recalculate the value, that is why you use the getter (like in this.getterA + this.getterB) and not CALL the getter (like this.getterA() this.getterB()).
If you still don't understand, check my "fake" render function that replaces the template rendering:
let productIsInStock = () => product => { ... }
render(h) {
return h('button', {
'#click': () => this.addProductToCard(product),
':disabled': () => productIsInStock()(product), // using arrow fn instead of the getter
}
}
This is also know as currying in functional programming language.
You define getters in your store as a function. This function is called with the state, other getters (and in case of module also the root state and root getters). Based on that you return some value. Normally, that data is some value (e.g. an object, number, boolean, etc.)
getters: {
numberOfPolarBears (state) {
return state.polarBears.length;
}
}
Functions in javascript do not differ much from other data. A function like this can also be defined as some variable.
// Method 1
function ticklePolarBear (bear) {
bear.tickle();
}
// Method 2
var ticklePolarBear = function (bear) {
bear.tickle();
};
In either case, you would call it with:
ticklePolarBear(frostyTheBear);
Why is this important? While when you normally map a getter and that way get some data back, nothing prevents you from mapping a getter and returning a function that you can call later.
getters: {
namedPolarBear (state) {
return function (index) {
if (index >= state.polarBears.length) {
return null;
}
return state.polarBears[index].name;
}
}
}
The arrow function sets the context of this differently, but is otherwise very similar to the example above.
computed in a component provides (in this case) a getter function for some attributes. In your example, it returns what the getter in your store returns, namely a function. Since it is a function, you can call it.
In this case no reactivity is going on, so you could also write the following:
data () {
return {
productIsInStock: this.$store.getters.productIsInStock
};
}

this.$set cant sort after

So have Vue adding an object to a list of objects but no matter what I do it doesn't seem to sort or append to the top of the list.
the set happens here
watch: {
sendBetData() {
// Creates the object to be appended to the list
const latest = {
bet_id: this.sendBetData.bet_id,
username: this.sendBetData.username,
bet: this.sendBetData.bet_amount,
client_seed: this.sendBetData.client_seed.seed,
created_at: this.sendBetData.created_at,
high: this.sendBetData.high,
multiplier: this.sendBetData.multiplier,
profit: this.sendBetData.profit,
result: this.sendBetData.result,
roll: this.sendBetData.roll_number,
server_seed: this.sendBetData.server_seed.seed_hash,
threshold: this.sendBetData.threshold,
user_id: this.sendBetData.user_id
};
this.$set(this.bets, latest.bet_id, latest)
},
},
then I have a computed function sorting
computed: {
bets() {
console.log('yep');
return _.orderBy(this.bets, 'created_at')
}
},
But no matter what I try it always sets it to the bottom of the list on the view
If you have an array of objects, just push it:
this.bets.push(latest)
Also, your orderBy method should probably be sortBy given what you want to do:
return _.sortBy(this.bets, bet => bet.created_at)

Can Vue.js update a nested property?

I have a complicated component with lots of two way bound variables so in order to keep things clean im grouping variables in category objects.
settings: {
setting1: true,
setting2: false,
setting3: true
},
viewMode: {
option1: true,
option2: false,
options3: true
}
I am then passing the settings to my component like so
<some-component :settings.sync="settings" :viewmode.sync="viewMode"></some-component>
some-component then can transform these values and emit them back to the parent but this is where the problem lies. It appears
this.$emit('update:settings.setting1', newValue)
does NOT work in Vuejs. The only solution i can find to update these values from some-component is to overwrite the entire settings object like so
props: {
settings: {
type: Object,
default: () => {
return {
setting1: true,
setting2: false,
setting3: true
}
}
}
},
computed: {
localSetting1: {
get () {
return this.settings.setting1
},
set (newValue) {
// This does not work
this.$emit('update:settings.setting1', newValue)
// The only thing that does seem to work, is overwriting the entire object
this.$emit('update:settings', {
setting1: newValue,
setting2: this.settings.setting2,
setting3: this.settings.setting3
}
// or to be less verbose, but still update the entire object
this.$emit('update:settings', Object.assign(this.settings, {settings1: newValue}))
}
}
}
This seems a bit messy. Is there not a way to update just a single nested property and emit it back to the parent? The most ideal way being something similar to this
this.$emit('update:settings.setting1', newValue)
Actually Vue can update nested props, we just need to overwrite the whole prop object, as Vue cannot track nested changes.
If we have a prop like this
props: {
someValues: {
'a': 1,
'b': 2,
'c': 3
}
}
If we make a change like this one Vue will not react
this.someValues.a = 10
What we need to do
this.tmpSomeValues = { ...someValues, a: 10 }
this.someValues = this.tmpSomeValues

Trouble triggering reactivity on changed Vuex objects

I'm modifying the value of an existing property on an object that is in an array of objects in my Vuex.store. When I update the store, it is not triggering a re-render of my computed property that is accessing the store. If I reset the stored value to an empty array, and then set it again to my new array, it'll trigger the change. But simply updating the property of the array of objects does not trigger a change.
I have tried using Vue.set() like the docs talk about, and that updates the store, but still does not trigger a re-render of the computed property. What am I missing? Using Vue 2.2.4 and Vuex 2.2.0.
//DEBUG: An example of the updated post I'm adding
let myNewScheduledPost = {
id: 1,
name: 'James'
};
this.$store.dispatch('addScheduledPost', post);
//DEBUG: My store
const options = {
state: {
scheduledPosts: [
{ id: 1, name: 'Jimmy'}
],
},
mutations: {
scheduledPosts: (state, scheduledPosts) => {
//This triggers the reactivity/change so my computed property re-renders
//But of course seems the wrong way to do it.
state.scheduledPosts = [];
state.scheduledPosts = scheduledPosts;
//Neither of these two lines triggers my computed property to re-render, even though there is a change in scheduledPosts
state.scheduledPosts = scheduledPosts;
Vue.set(state, 'scheduledPosts', scheduledPosts);
},
},
actions: {
addScheduledPost({ commit, getters }, newScheduledPost) {
let scheduledPosts = getters.scheduledPosts;
const idx = scheduledPosts.findIndex(existingScheduledPost => existingScheduledPost.id === newScheduledPost.id);
//If the post is already in our list, update that post
if (idx > -1) {
scheduledPosts[idx] = newScheduledPost;
} else {
//Otherwise, create a new one
scheduledPosts.push(newScheduledPost);
}
commit('scheduledPosts', scheduledPosts);
//DEBUG: This DOES have the correct updated change - but my component does not see the change/reactivity.
console.log(getters.scheduledPosts);
}
},
getters: {
scheduledPosts: (state) => {
return state.scheduledPosts;
}
}
};
//DEBUG: Inside of my component
computed: {
mySortedPosts()
{
console.log('im being re-rendered!');
return this.$store.getters.scheduledPosts.sort(function() {
//my sorted function
});
}
}
Your problem is if you are wanting to access a portion of the state you don't use a getter https://vuex.vuejs.org/en/state.html.
computed: {
mySortedPosts(){
return this.$store.state.scheduledPosts
}
}
Getters are for computed properties in the store https://vuex.vuejs.org/en/getters.html. So in your case you might create a getter to sort your scheduled posts then name it sortedScheduledPosts and then you can add it to your components computed properties like you are now.
The key thing is your getter needs to have a different name then your state property just like you would in a component.

Categories