custom function changes Vue.js data value as side effect - javascript

I have a function inside the methods property which takes passedData value from the data() method, does some changes to the object and stores the new value in a constant
this somehow causes a side effect which changes the passedData value also.
What is the cause and how can I prevent that ?
this.passedData: -->
{"propss":"value"}
App.vue?3dfd:61 {"propss":"propss : value"}
App.vue?3dfd:49 {"propss":"propss : value"}
App.vue?3dfd:61 {"propss":"propss : propss : value"}
new Vue({
el: "#app",
data() {
return {
passedData: { propss: "value" },
};
},
methods: {
displayData() {
console.log(JSON.stringify(this.passedData));
const root = d3.hierarchy(this.passedData, function(d) {
if(typeof d == "object")
return Object.keys(d).filter(d=>d!="$name").map(k=>{
if(typeof d[k] == "object") d[k].$name = k;
else d[k] = k + " : " + d[k];
return d[k];
});
})
console.log(JSON.stringify(this.passedData));
},
},
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="app">
<button type="button" #click="displayData">display</button>
</div>

The problem you are facing here is, that data in vue.js is reactive. So changing data stored anywhere will change the source of the data too.
If you want to change something without facing issues due to reactivity, you need to assign this data without reference as you need. Small example:
// Passing your reactive data
passedData: { propss: "value" },
newData: {}
// Passing data to new property and reactivity
this.newData = this.passedData
this.newData.propss = 'newValue' // this will change passedData.propss to "newValue" too
// Assign Data without reference
this.newData = JSON.parse(JSON.stringify(this.passedData))
If you now change newData it wont affect passedData, as JSON.parse(JSON.stringify(this.passedData)) created a new state without reference to the original.
Note that this should only be a workaround, it isn´t a proper state management.

Related

Why isn't the mobx #computed value?

Simple: the computed value isn't updating when the observable it references changes.
import {observable,computed,action} from 'mobx';
export default class anObject {
// THESE WRITTEN CHARACTERISTICS ARE MANDATORY
#observable attributes = {}; // {attribute : [values]}
#observable attributeOrder = {}; // {attribute: order-index}
#observable attributeToggle = {}; // {attribute : bool}
#computed get orderedAttributeKeys() {
const orderedAttributeKeys = [];
Object.entries(this.attributeOrder).forEach(
([attrName, index]) => orderedAttributeKeys[index] = attrName
);
return orderedAttributeKeys;
};
changeAttribute = (existingAttr, newAttr) => {
this.attributes[newAttr] = this.attributes[existingAttr].slice(0);
delete this.attributes[existingAttr];
this.attributeOrder[newAttr] = this.attributeOrder[existingAttr];
delete this.attributeOrder[existingAttr];
this.attributeToggle[newAttr] = this.attributeToggle[existingAttr];
delete this.attributeToggle[existingAttr];
console.log(this.orderedAttributeKeys)
};
}
After calling changeAttribute, this.orderedAttributeKeys does not return a new value. The node appears unchanged.
However, if I remove the #computed and make it a normal (non-getter) function, then for some reason this.orderedAttributeKeys does display the new values. Why is this?
EDIT: ADDED MORE INFORMATION
It updates judging by logs and debugging tools, but doesn't render on the screen (the below component has this code, but does NOT re-render). Why?
{/* ATTRIBUTES */}
<div>
<h5>Attributes</h5>
{
this.props.appStore.repo.canvas.pointerToObjectAboveInCanvas.orderedAttributeKeys.map((attr) => { return <Attribute node={node} attribute={attr} key={attr}/>})
}
</div>
pointerToObjectAboveInCanvas is a variable. It's been set to point to the object above.
The changeAttribute function in anObject is called in this pattern. It starts in the Attribute component with this method
handleAttrKeyChange = async (existingKey, newKey) => {
await this.canvas.updateNodeAttrKey(this.props.node, existingKey, newKey);
this.setState({attributeEdit: false}); // The Attribute component re-renders (we change from an Input holding the attribute prop, to a div. But the above component which calls Attribute doesn't re-render, so the attribute prop is the same
};
which calls this method in another object (this.canvas)
updateNodeAttrKey = (node, existingKey, newKey) => {
if (existingKey === newKey) { return { success: true } }
else if (newKey === "") { return { success: false, errors: [{msg: "If you'd like to delete this attribute, click on the red cross to the right!"}] } }
node.changeAttribute(existingKey, newKey);
return { success: true }
};
Why isn't the component that holds Attribute re-rendering? It's calling orderedAttributeKeys!!! Or am I asking the wrong question, and something else is the issue...
An interesting fact is this same set of calls happens for changing the attributeValue (attribute is the key in anObject's observable dictionary, attributeValue is the value), BUT it shows up (because the Attribute component re-renders and it pulls directly from the node's attribute dictionary to extract the values. Again, this is the issue, an attribute key changes but the component outside it doesn't re-render so the attribute prop doesn't change?!!!
It is because you have decorated changeAttribute with the #action decorator.
This means that all observable mutations within that function occur in a single transaction - e.g. after the console log.
If you remove the #action decorator you should see that those observables get updated on the line they are called and your console log should be as you expect it.
Further reading:
https://mobx.js.org/refguide/action.html
https://mobx.js.org/refguide/transaction.html
Try to simplify your code:
#computed
get orderedAttributeKeys() {
const orderedAttributeKeys = [];
Object.entries(this.attributeOrder).forEach(
([attrName, index]) => orderedAttributeKeys[index] = this.attributes[attrName])
);
return orderedAttributeKeys;
};
#action.bound
changeAttribute(existingAttr, newAttr) {
// ...
};
Also rename your Store name, Object is reserved export default class StoreName

Vuex action with ajax not updated in computed

I use Vue with Vuex. In one case I use Ajax to get a presentation value. Somewhere on the way, probably in computed it's no longer reactive.
In my component:
props: [ 'x', 'y' ],
template: `
<div class="presentation">
{{ presentation }}
</div>
`,
computed: {
presentation() {
return this.$store.getters.presentation({ x: this.x, y: this.y });
}
}
Vuex store:
const store = new Vuex.Store({
state: {
table: {
data: []
}
},
...
Vuex actions:
I call an url with ajax and return a promise. I also commit a mutation.
actions: {
save: (context) => {
let uri = 'some/uri';
let params = {};
let value = 'A value';
return axios
.post(uri, params)
.then((response) => {
context.commit('setPresentation', value);
})
.catch((error) => {
console.log('error');
})
.finally(() => {});
},
},
Vuex mutations:
mutations: {
setPresentation: (state, value) => {
let pos = state.table.pos;
state.table.data[pos.y][pos.x].presentation = value;
},
}
Vuex getters:
getters: {
presentation: (state) => (pos) => {
return state.table.data[pos.y][pos.x].presentation;
}
},
I've already make sure of the following:
I set up the table.data state to a default value to make it reactive
Using a getter to get the data
Using an action for the ajax call
Call a mutation with a commit in the action
Notes:
The ajax call needs to be in an action and not in created, because I'm going to use presentation from more than one component.
I prefer a solution which does not need external Vue plugins.
Question(s)
What did I miss?
How can I solve it in the best way?
You need to use Vue.set instead of state.table.data[pos.y][pos.x].presentation = value;
See https://v2.vuejs.org/v2/guide/list.html#Caveats for details
Try to update your mutation with the following code:
if (!state.table.data[pos.y]) {
Vue.set(state.table.data, pos.y, [])
}
Vue.set(state.table.data[pos.y], pos.x, { presentation: value })
A word from me, the OP (Original poster):
Why it failed the first times was that I only set the last part { presentation: value } with Vue.set as I already has pos.y and pos.x set in another ajax call.
For Vue to be fully aware if the change I needed to set everything that has not already been set in state, with Vue.set. So I needed use Vue.set to set pos.y and pos.x as well.
Also see another excellent answer below.
Vue cannot detect changes to an array when you directly set an item with the index
Your mutation is OK; there's no issues there. Vue will detect the assignment to the presentation property of the object just fine as long as the object is being observed by Vue.
In most cases Vue will automatically observe objects, but there are some quirks (especially with arrays) that you need to be aware of.
Vue cannot detect changes to an array when you directly set an item with the index (docs).
I assume you are populating your arrays in the following manner:
for (let y = 0; y < Y_MAX; y++) {
// This is bad! Vue cannot detect this change
state.table.data[y] = []
for (let x = 0; x < X_MAX; x++) {
// Same as above, Vue cannot detect this change. As a consequence,
// the object you are assigning to the array will not be observed
// by Vue! So if you were to mutate the presentation property
// of this object, Vue won't know about it.
state.table.data[y][x] = { presentation: '...' }
}
}
So to fix your problem you just need to make sure you are not mutating arrays in the following way:
array[index] = whatever
You need to do this instead:
Vue.set(array, index, whatever)
Alternatively, you can build the array first and then assign it to state.table.data last; Vue will detect the assignment and then recursively observe the array and everything contained within it.
const data = []
for (let y = 0; y < Y_MAX; y++) {
data[y] = []
for (let x = 0; x < X_MAX; x++) {
data[y][x] = { presentation: '...' }
}
}
// After this assignment, data (and all objects contained within it)
// will be observable
state.table.data = data
It looks like your props is an array. Are you sure this.x and this.y return the correct values inside your presentation method?

Ember component computed function does not rerun when data changes

I have stored a string value within a computed property like so:
clientId: Ember.computed.oneWay("_clientId")
where _clientId is defined as a property on the object like so:
export default Ember.service.extend { _clientId: null, clientId: Ember.computed.oneWay("_clientId"), updateId() {this.set("_clientId", "fe48822d-bf50-44a1-9ce0-61b06504d726"); } }
I then have a component with a computed property like so:
chartData: Ember.computed("data", function () {
const data = this.get("data");
const clientId = this.get("session").get("clientId");
console.log(clientId);
if (data && data.get("left") !== undefined && data.get("right") !== undefined) {
return data;
}
this.set("_chartDisplayData", null);
return null;
})
When I called updateId, i expected the chartData function to be re-run as the value of the clientId is changed (i verified that the value gets changed for clientId). However, the chartData function never re-runs, why is this?
You need to tell the computed property about all of your dependencies. First, the computed property will never run if it isn't being used somewhere. If you aren't using it you need an observer instead. But assuming you are actually using it, the computed property will only recompute itself when one of the listed dependencies change. And if you list an object as a dependency, it will not update if only some of the object's properties are changed, only if the entire object is replaced. Try this:
chartData: Ember.computed("data.left", "data.right", "session.clientId", function () {
const data = this.get("data");
const clientId = this.get("session.clientId");
console.log(clientId);
if (data && data.get("left") !== undefined && data.get("right") !== undefined) {
return data;
}
this.set("_chartDisplayData", null);
return null;
})

Vue.js computed value never run on startup

My computed values get function is never run when the vm is created, so the value it should have is never assigned.
Strangely enough it's run if I try to access the value, it is just never run when the app starts.
Basically I'm supposed to be computing the value of the skin and on startup or when changed the stylesheet link should be altered to load the correct skin.
It works great, the problem is that it's not being run on startup. Do I have to use some hacky solution, like, getting the value once in the mounted function of the vm? Or am I missing something here...
computed: {
skin: {
get: function () {
var mySkin = "my/skin/string";
if (window.localStorage.getItem("skin") === null) {
window.localStorage.setItem("skin", mySkin);
jquery("link[id='skin']").attr("href", "css/skins/" + mySkin + ".css");
} else {
mySkin = window.localStorage.getItem("skin");
window.localStorage.setItem("skin", mySkin);
jquery("link[id='skin']").attr("href", "css/skins/" + mySkin + ".css");
}
var hey = this.skin;
return mySkin;
},
set: function (val) {
window.localStorage.setItem("skin", val);
jquery("link[id='skin']").attr("href", "css/skins/" + val + ".css");
}
}
}
Computed property is a property which depends on data properties.
In your case it is better IMO to create a data property skin, and a method like setSkin() which initializes the skin data property, and another method saveSkin() to save your property into localStorage.
You then can run setSkin() in mounted() hook.
Template:
new Vue({
el: '#app',
data: {
skin: ''
},
methods: {
setSkin: function() {
this.skin = ...;
},
saveSkin: function() { // call this function when you need to save skin data into localStorage
window.localStorage.setItem("skin", this.skin);
....
}
},
mounted() {
this.setSkin(); // retrieve skin data from localStorage, etc.
}
});

VueJS $set with variable keypath

I'm currently working on a simple filemanager component which I trigger from parent component. After selecting media in the filemanager I $dispatch a simple data object with 2 keys: element & media. I use element to keep track where I want the media to be appended to my current data object and media has the media information (id, type, name and so on). This setup gives me some trouble when I want to $set the media data to variables within my data object. The variables are locales, so: nl-NL, de-NL and so on.
setMediaForPage : function(data){
if(!this.page.media[this.selectedLanguage]['id'])
{
// set for all locales
var obj = this;
this.application.locales.forEach(function(element, index, array) {
obj.$set(obj.page.media[element.locale], data.media);
})
}
else
{
// set for 1 locale
this.$set(this.page.media[this.selectedLanguage], data.media);
}
}
What happens when I run this code is that the data object shows up properly in Vue Devtools data object, but the media does not show up in the template. When I switch the language (by changing the this.selectedLanguage value), the media does show up.
I think this has to do with the variables in the object keypath, but I'm not 100% sure about that. Any thoughts on how to improve this code so I can show the selected media in the parent component without having to change the this.selectedLanguagevalue?
I don't know your data structure exactly, but you can certainly use variables as the the keypath in vue, however remember that the keyPath should be a string, not an object.
If your variable that you want to use in the keypath is part of the vue, you'd do it like this:
obj.$set('page.media[element.locale]', data.media)
... because the keyPath which is a string is intelligently parsed by Vue's $set method and is of course it knows that this path is relative to the $data object.
new Vue({
el: '#app',
data() {
return {
msg: "hello world",
attr: {
lang: {
zh: '中文',
en: 'english'
}
}
}
},
methods: {
$set2(obj, propertyName, value) {
let arr = propertyName.split('.');
let keyPath = arr.slice(0, -1).join('.');
let key = arr[arr.length - 1];
const bailRE = /[^\w.$]/
function parsePath(obj, path) {
if (bailRE.test(path)) {
return
}
const segments = path.split('.')
for (let i = 0; i < segments.length; i++) {
if (!obj) return
obj = obj[segments[i]]
}
return obj
}
let target = parsePath(obj, keyPath);
// console.log(target, key);
// target[key] = value;
this.$set(target, key, value);
}
},
mounted() {
setTimeout(() => {
// this.$set('attr.lang.zh', '嗯');
// this.$set2(this, 'attr.lang.zh', '嗯');
this.$set2(this.attr, 'lang.zh', '嗯');
}, 1000);
}
})
调用示例:this.$set2(this.attr, 'lang.zh', '嗯');
i have also experienced similar problems,remove variables -,these variables nl-NL, de-NL change to nlNl, deNl
and i not use
obj.$set('page.media[element.locale]', data.media)
but
obj.$set('page.media.'+element.locale, data.media);
then it work

Categories

Resources