vue.js watch not updated - javascript

I'm new to vue.
I'm now trying to update a couple of variables based on the change of another computed variable.
This computed variable is taking the values from a Vuex store and works as should. I see the values change.
In order to calculate the derived variables I've created a watch that watches the computed variable and then updates these derived values.
This watch is called two times during start-up and then no longer, although the computed values keeps updating.
What am I doing wrong.
This is working:
...
computed: {
lastAndMarkPrice() {
return store.getters.getLastAndMarkPriceByExchange(
"deribit",
store.getters.getAsset
);
},
...
this part is not working:
...
data: () => ({
lastPriceUp: false,
lastPriceDn: false,
markPriceUp: false,
markPriceDn: false,
}),
...
watch: {
lastAndMarkPrice (newValue, oldValue) {
console.log(newValue, oldValue);
this.lastPriceUp = newValue.lastPrice > oldValue.lastPrice;
this.lastPriceDn = newValue.lastPrice < oldValue.lastPrice;
this.markPriceUp = newValue.markPrice > oldValue.markPrice;
this.markPriceDn = newValue.markPrice < oldValue.markPrice;
},
},
...

By default a watch is shallow. If a new object is assigned to lastAndMarkPrice then the handler will be called but it won't check for mutations of properties within that object.
To create a deep watcher you'd do something like this:
watch: {
lastAndMarkPrice: {
deep: true,
handler (newValue, oldValue) {
// ...
}
}
}
https://v2.vuejs.org/v2/api/#watch
Usually that would be the correct solution but your use-case is slightly more complicated because you need access to the old values. Using a deep watcher won't help with that as you'll just be passed the same object.
To get around that problem you'll need to take copies of the old values somewhere so that you still have them available to compare with the new values. One way to do that would be to have the computed property take a copy:
computed: {
lastAndMarkPrice() {
const prices = store.getters.getLastAndMarkPriceByExchange(
"deribit",
store.getters.getAsset
);
// I'm assuming that prices is initially null or undefined.
// You may not need this bit if it isn't.
if (!prices) {
return null;
}
return {
lastPrice: prices.lastPrice,
markPrice: prices.markPrice
}
}
}
With the code above, each time the values of lastPrice or markPrice change it will re-run the computed property and create a new object. That will trigger the watch handler and, importantly, you'll get two different objects passed as the old and new values. You don't need to use deep in this case as the object itself is changing, not just the properties within it.
You could also shorten it a little with...
return { ...prices }
...rather than explicitly copying the two properties across.

Related

Why doesn't custom hook return updated data?

There is a custom useMarkers hook for converting markers into clusters on the map.
It takes needed props,but the essential one are options prop.
const { markersArray, supercluster } = useMarkers({
points: transformedMarkers,
bounds,
zoom,
options: defaultOptions2, // <=== HERE IS WHAT WE NEED
});
And here is it from the inside:
const initialMarkersData = {
markersArray: [],
supercluster: []
};
const useMarkers = (initialData = {}) => {
const [markersData, setMarkersData] = useState(initialMarkersData);
const { clusters, supercluster } = useSupercluster(initialData);
useEffect(() => {
if (initialData.zoom <= 17) {
// console.log("SET NON-CLUSTERS");
setMarkersData({
...markersData,
markersArray: initialData.points,
});
} else {
// console.log("SET CLUSTERS");
setMarkersData({
markersArray: clusters,
supercluster,
});
}
}, [initialData.zoom, clusters, supercluster]);
return markersData;
};
Inside useMarkers I`m passing props to another hook, useSupercluster (comes from separate npm library), and depending on options prop it should return an updated clusters array, and depending on the zoom level an additional useEffect should change the markersData, which is then returns as a result.
And the problem is that when I add a condition for passing different options, the new array is not returned.
For example, if I pass options like this, it works:
options: defaultOptions2,
But if I add condition instead of just single defaultOptions2
options: zoom < 17 ? defaultOptions : defaultOptions2
It doesn't work. If I console.log initialData inside useMarkers hook, I will see new options, so the condition works correct, but although this hook is re-renders, it looks like useSupercluster() doesn`t reinitialize again because it doesn't return modified array according to new options. What could be the reason?
UPDATE*: I tried to investigate this behavior with debugger and everything looks correct. The supercluster property, which is destructured from useSupercluster hook, has to have the this options in it, and although new options successfully passed to useSupercluster within initialData, supercluster property got the empty object at the end;

Is my Vue.js computed function recursive?

I have a vue computed function and would like to know if there is a recursion in my code.
I have some comma seperated states within my prop this.searchQuery['by-multiple-states']. My computed function statesSearchQuery should be a 'wrapper' for my prop, it should just give me a way to work with my prop not as a string but as a array.
The getter just returns the states as array. The setter should toggle a given 'val'. If within the array, remove it, else add it. Finaly apply the changes on the prop itself.
Because the getter and setter is both called with this.statesSearchQuery and the getter is used within the first line of my setter, I'm not sure if there would be a weird interaction. Could someone enlighten me about this situation?
computed: {
statesSearchQuery: {
get() {
return this.searchQuery['by-multiple-states'].split(',').filter(t => t !== '');
},
set(val) {
let state = this.statesSearchQuery;
if (state.includes(val)) {
state = state.filter(t => t !== val);
} else {
state.push(val);
}
this.searchQuery['by-multiple-states'] = state.join(',');
},
},
},
Update: In retrospect, I can rephrase the question. Is it ok to use the getter of a computed function in the setter of the same?

Can't change object property for Vue component (a true anomaly)

A strange anomaly. An object/component property this.gridTiles does get set correctly when I do the "undo" action first. But then when I perform the "redo" action (see code below), I'm unable to set this.gridTiles to the new value! It seems to be holding on to the old value. this.gridTiles is an array with nested items/objects. Right before I try to set the value though, it's giving me the correct value if I assign it to a test variable. Very strange! Any help would be much appreciated!
Note : cloneDeep() is enabled by this package : [https://www.npmjs.com/package/clone-deep]
ComponentA.vue
data() {
return {
gridTiles: [],
}
},
....
setCurrentEntireState(historyParams) {
let test = cloneDeep(historyParams.gridTiles); // CORRECT VALUE
this.test = cloneDeep(historyParams.gridTiles); // we can set this arbitrary object property correctly
//this.gridTiles = test; // doesn't work
//delete this.gridTiles; // doesn't help even if we do this first
this.gridTiles = cloneDeep(historyParams.gridTiles); // WRONG VALUE, WHY ??
},
getCurrentEntireState() { // used in saving historyStack items, not shown
return {
gridTiles: cloneDeep(this.gridTiles)
}
},
....
EventBus.$on('refreshHistoryStateForReceiptMap', (historyParams) => {
this.setCurrentEntireState(historyParams);
....
})
ComponentB.vue
methods: {
....
undoHistoryAction() {
let historyParams = this.$store.getters.historyStack[this.$store.getters.historyIndex - 1];
EventBus.$emit('refreshHistoryStateForReceiptMap', historyParams);
this.$store.commit('historyIndexDecrement');
},
redoHistoryAction() {
let historyParams = this.$store.getters.historyStack[this.$store.getters.historyIndex];
EventBus.$emit('refreshHistoryStateForReceiptMap', historyParams);
this.$store.commit('historyIndexIncrement');
}
},
This may not be the correct answer and should maybe be a comment, but is too long to be a comment so I'll post here. Hopes this helps:
The code:
setCurrentEntireState(historyParams) {
let test = cloneDeep(historyParams.gridTiles); // CORRECT VALUE
this.test = cloneDeep(historyParams.gridTiles); // we can set this arbitrary object property correctly
//this.gridTiles = test; // doesn't work
//delete this.gridTiles; // doesn't help even if we do this first
this.gridTiles = cloneDeep(historyParams.gridTiles); // WRONG VALUE, WHY ??
},
gets wrong every line that uses this. I would bet that this code:
EventBus.$on('refreshHistoryStateForReceiptMap', (historyParams) => {
this.setCurrentEntireState(historyParams);
....
})
Is somehow messing with the this context. Maybe is placed inside a callback so it loses the this context of the component?
You should log this inside setCurrentEntireState to check if it really is the component.
The console.log() was showing a different value than when I actually used the Chrome JS debugger with a breakpoint. Also, I investigated further downstream and found that some other custom code was reverting the value back to the old/original one. The moral of the story, console.log() might not always be correct, or there might be some lag?

Ember - Custom Computed Property to check if all dependent fields exists

I am creating a form and I am trying to find a simple, elegant way of handling to see if all inputs exist.
Form = Ember.Object.extend({
// section 1
name: null,
age: null,
isABoolean: null,
// section 2
job: null,
numberOfSiblings: null,
isComplete: Ember.computed.and('_isSection1Complete', '_isSection2Complete'),
_isSection1Complete: function() {
var isPresent = Ember.isPresent;
return isPresent(this.get('name')) && isPresent(this.get('age')) && isPresent(this.get('isABoolean'));
}.property('name', 'age', 'isABoolean'),
_isSection2Complete: function() {
var isPresent = Ember.isPresent;
return isPresent(this.get('job')) && isPresent(this.get('numberOfSiblings'));
}.property('job', 'numberOfSiblings')
});
However, this doesn't seem to scale. My actual application will have many sections (over 20 sections).
I am looking into trying to create a re-usable computed property that fits my needs. Take for example the code of what I am going for:
Form = Ember.Object.extend({
// properties...
isComplete: Ember.computed.and('_isSection1Complete', '_isSection2Complete'),
_isSection1Complete: Ember.computed.allPresent('name', 'age', 'isABoolean'),
_isSection2Complete: Ember.computed.allPresent('job', 'numberOfSiblings')
});
I feel that this is a common case, but I'm failing to find the correct computed properties on how to execute this, so I would like to make my own.
Two questions:
Where's the best place to define the custom computed property? Can I just attach a function to Ember.computed?
Is there an easier way to solve this? I feel like I'm overlooking something simple.
As for Question #1,
You can define a custom computed helper in the App namespace. In this example, I created a new computed helper called allPresent that checks each property passed in against Ember.isPresent.
App.computed = {
allPresent: function (propertyNames) {
// copy the array
var computedArgs = propertyNames.slice(0);
computedArgs.push(function () {
return propertyNames.map(function (propertyName) {
// get the value for each property name
return this.get(propertyName);
}, this).every(Ember.isPresent);
});
return Ember.computed.apply(Ember.computed, computedArgs);
}
};
It can be used like this, per your example code:
_isSection2Complete: App.computed.allPresent(['job', 'numberOfSiblings'])
I adapted this from the approach here: http://robots.thoughtbot.com/custom-ember-computed-properties
As for Question #2, I can't think of a simpler solution.
I had to make a minor adjustment to Evan's solution, but this works perfectly for anyone else that needs it:
App.computed = {
allPresent: function () {
var propertyNames = Array.prototype.slice.call(arguments, 0);
var computedArgs = propertyNames.slice(0); // copy the array
computedArgs.push(function () {
return propertyNames.map(function (propertyName) {
// get the value for each property name
return this.get(propertyName);
}, this).every(Ember.isPresent);
});
return Ember.computed.apply(Ember.computed, computedArgs);
}
};
This can now be used as such:
_isSection2Complete: App.computed.allPresent('job', 'numberOfSiblings')

Store state of a JavaScript Object

Im trying to store the stats of 'this' in my javscript object so that later on in my application I can return 'this' to a previous state. I thought I could accomplish using a closure but so far I haven't successful. My idea was to do something like this
function SavedFeature() {
var self = this;
this.savedItem;
this.storeState = function() {
this.savedItem = storeClosure();
}
function storeClosure() {
var closure = self;
return function() {
return closure;
};
};
//other things the user can change...
}
so later on in my application if I needed to return to the point when I called storeState I could just do
//return the object I put in my closure
var backToNormal = savedFeature.savedItem();
that doesn't work though because any changes to my savedFeature object after I call storeState() are being reflected in the item im retrieving from called savedItem(). I'm guessing this is happening because closure is being set to a reference of self instead of copied to a new instance.
Is there anyway to store the state of my entire object in a closure like this or do I need to store this some other way.
The issue you are running into is that in js objects are passed by reference. This means that all changes performed on your object will apply to your obj.savedItem property.
Fix: Store a deep clone into obj.savedItem
this.storeState = function() {
this.savedItem = _.cloneDeep(this); // or _.clone(this, true);
}
cloneDeep is a lodash method, most js libs supply one of their own, e.g. jQuery's $.extend, etc.
You could easily roll your own deep clone function, look up the options on this thread.
A complete example with jQuery:
function SavedFeature() {
this.savedItem;
this.clone = function() {
return $.extend(true, {}, this);
},
this.storeState = function() {
this.savedItem = this.clone();
}
}
Doing it this way allows you adapt to different environments by changing your clone method as it is facading the used library method.
There are dozens of ways how to implement it. I will do just simple one. saving property.
Take into account if you want to save entire object you need to do deep copy of the object.
this is your feature:
function SavedFeature() {
this.savedItem = {'isNew': true};
this.stateMachine = new StateMachine();
}
this is some kind of state machine:
function StateMachine () {
var state = { 'isNew' : null};
function set(newState) {
state.isNew = newState.isNew;
}
function get() {
return state.isNew;
}
return {
get : get,
set : set
};
}
which, know how to store isNew property
and a working sample:
var savedFeature = new SavedFeature();
console.log(savedFeature.savedItem); // true by default
savedFeature.stateMachine.set(savedFeature.savedItem); // saving state.
savedFeature.savedItem.isNew = false; // modifying state
console.log(savedFeature.savedItem); // return false, because of statement above
var restoredState = savedFeature.stateMachine.get(); // restoring state
console.log(restoredState); // true
savedFeature.savedItem.isNew = restoredState.isNew;
console.log(savedFeature.savedItem); // true
you can adjust that code, and reach functionality whatever you need. hope that helps

Categories

Resources