Make only nested property reactive - vue.js

I am having a data object that consists of properties unrelated to vue/the UI and data that describes the state. Now I only want the state to be reactive but I need the entire object initially in the component. I need that vue not modifies the other properties because it messes with another library accessing the other properties. (expects array but gets observer)
Is it possible to only make part of an object reactive?
class Game {
constructor() {
this.untrackedProperty = ...;
this.state = {
these: "",
should: "",
be: "",
reactive: ""
}
}
}
// vue component
<script>
export default {
data: function() {
return {
gameState: null
}
},
created() {
this.game = new Game();
this.gameState = this.game.state;
}
}
</script>
Something like that.
I meant it as "That's how I think it should work - it doesn't, but I think it describes pretty well my intentions"

How about passing into your component 2 values:
state this will be tracked
untrackedProperty this will not be tracked
Instead of passing Game as a whole object, separate those 2 values

Related

How do I watch changes to an object? this.$set seems to capture only property update

I have this object of arrays that I'm tryin to watch every update of.
myData = {
"299":[527],
"376":[630,629]
}
I read this documentation on watching an object which instructed to use either this.$set(object, propertyName, value) or Object.assign({}, this.object, dataToBeAppended) to watch an object. I used this.$set.
export default {
...
data() {
return {
myData: {},
};
},
watch: {
myData(newVal) {
console.log(`🔴localStorage`);
},
},
methods: {
onFoldChange(propertyName) {
const newArr = [...]
this.$set(this.myData, propertyName, newArr);
},
}
}
Unlike what I expected, vue captures changes on property only. Changes in value to an existing property are not being watched. For example, if a property "299" was newly added, it will print 🔴localStorage. When the value of a property "299" is updated from [527] to something else, nothing is fired. When I print myData, I see every value updated correctly. It is just that watch isn't capturing the changes.
The documentation also described we can watch an array using this.$set(this.myData, indexOfItem, newValue) so I also tried array version of the above code, like this.
this.$set(this.myData[propertyName], index, newValueToAdd);
This time it doesn't listen at all. Not even the first entry.
Is there any better way to solve this issue? How do others watch an object? Is the complication coming from the type of values (array) ?
Currently, myData watcher observes only an object. Object contains pointers to arrays as in JS Objects & Arrays are passed by reference not by copy. That's why it can detect only changes in keys and with simple values. If you want to observe it deeper - I mean also those subarrays (or subobjects) - just use deep watch.
watch: {
myData: {
deep: true,
handler (newVal) {
console.log(`🔴localStorage`);
}
}
}
Another possible solution could be to use some Array.prototype operation to modify an array if it already exists. E.g:
methods: {
onFoldChange(propertyName) {
if (propertyName in this.myData && Array.isArray(this.myData[propertyName])) {
this.myData[properyName].push(162) // Some random value
} else {
const newArr = [...]
this.$set(this.myData, propertyName, newArr);
}
},
}

vue watch handler called without actual changes

In this codepen have a counter that can be incremented with a "incr" link.
I now have a computed property and a watch:
computed: {
test() {
let unused = this.counter;
return [42];
}
},
watch: {
test(val, old) {
// Should I avoid firing when nothing actually changed
// by implementiong my own poor-man's change detection?
//
// if (JSON.stringify(newVal) == JSON.stringify(oldVal))
// return;
console.log(
'test changed',
val,
old
);
}
}
A contrived example perhaps, but in reality this is a calculation where the real data is reduced (in a vuex getter) and most-often, the reduced data doesn't change even when some of the data changes.
Edited to add more detail: The data in the vuex store is normalized. We're also using vue-grid-layout that expects its layoutproperty in a certain non-normalized format. So we have a gridLayout getter that does the vuex -> vue-grid-layout tranform. Watching this gridLayout getter fires even when the resulting gridLayout doesn't actually change, but other details do, such as names and other irrelevant-to-vue-grid-layout object keys in the vuex store.
Now in the above example, when this.counter changes, the watch on test fires too, even though the newVal and oldVal are "the same". They aren't == or === mind you, but "the same" as in JSON.stringify(newVal) == JSON.stringify(oldVal).
Is there any way to have my watch fire only when there are actual changes? Actually comparing JSON.stringify() seems inefficient to me, but I'm worried about performance problems as my project grows as my watch could do expensive operations and I want to ensure I'm not missing something.
According to the Vue.js documentation computed properties are reevaluated when ever a reactive dependency is changed.
In your case the reactive dependency is this.counter
You can achieve the same result by invoking a method as opposed to a computed property.
Just change up your component architecture:
data () {
return: {
counter: 0,
obj: {},
output: null
}
},
watch: {
counter(val, old) {
// Alternatively you could remove your method and do something here if it is small
this.test(val);
},
// Deep watcher
obj: {
handler: function (val, oldVal) {
console.log(val);
console.log(oldVal);
this.test(val);
},
deep: true
},
}
},
methods: {
test() {
let unused = this.counter;
if (something changed)
this.output = [42];
}
}
}
Now in your template (or other computed properties) output is reactive
Read more: https://v2.vuejs.org/v2/guide/computed.html#Computed-Caching-vs-Methods

How to pass an array of objects to child component in VueJS 2.x

I am trying to send an array containing arrays which in turn contains objects to one component from another, but the content from the array seems to be empty in the child component.
I have tried sending the data as a String using JSON.Stringify() and also as an array
My parent component:
data: function(){
return{
myLineItems : []
}
},
created(){
this.CreateLineItems();
},
methods:{
CreateLineItems(){
let myArrayData = [[{"title":"Title1","value":2768.88}],[{"title":"Title2","value":9}],[{"title":"Title3","value":53.61},{"title":"Title4","value":888.77},{"title":"Title5","value":1206.11},{"title":"Title6","value":162.5}]]
this.myLineItems = myArrayData;
}
}
My parent component's template:
/*
template: `<div><InvoiceChart v-bind:lineItems="myLineItems"></InvoiceChart></div>`
My child component:
const ChildComponent= {
props: {
lineItems: {
type: Array
}
},
mounted() {
console.log(this.lineItems);
}
};
The parent component is created as so (inside a method of our main component):
var ComponentClass = Vue.extend(InvoiceDetails);
var instance = new ComponentClass({
propsData: { invoiceid: invoiceId }
});
instance.$mount();
var elem = this.$refs['details-' + invoiceId];
elem[0].innerHTML = "";
elem[0].appendChild(instance.$el);
If I try to do a console.log(this) inside the childcomponent, I can see the correct array data exist on the lineItems property..but i can't seem to access it.
I have just started using VueJS so I haven't quite gotten a hang of the dataflow here yet, though I've tried reading the documentation as well as similar cases here on stackoverflow to no avail.
Expected result: using this.lineItems should be a populated array of my values sent from the parent.
Actual results: this.lineItems is an empty Array
Edit: The problem seemed to be related to how I created my parent component:
var ComponentClass = Vue.extend(InvoiceDetails);
var instance = new ComponentClass({
propsData: { invoiceid: invoiceId }
});
instance.$mount();
var elem = this.$refs['details-' + invoiceId];
elem[0].innerHTML = "";
elem[0].appendChild(instance.$el);
Changing this to a regular custom vue component fixed the issue
Code - https://codesandbox.io/s/znl2yy478p
You can print your object through function JSON.stringify() - in this case all functions will be omitted and only values will be printed.
Everything looks good in your code.
The issue is the property is not correctly getting passed down, and the default property is being used.
Update the way you instantiate the top level component.
Try as below =>
const ChildComponent= {
props: {
lineItems: {
type: Array
}
},
mounted() {
console.log(this.lineItems);
}
};

vue: how to make the object passed to component reactive?

Codepen Demo
I have a component which has an location object as props. The argument I passed in is locations[index] which is a selected item from a locations array.
However, the component cannot react when the index change. As you can see in the demo, the JSON change as you click the button, but the component cannot update.
What's the best way to make the component reactive?
Your location component populates the province and city data properties in the mounted hook only. When the location prop changes, the mounted hook will not be called again, so you are left with stale data.
Use a computed property instead:
computed: {
province() {
return this.location.province;
},
city() {
return this.location.city;
}
}
Updated codepen
If you really do require province and city to be data properties (and not computed properties) then you will need to watch the location prop to update the properties:
data() {
return {
province: null,
city: null
}
},
watch: {
location: {
immediate: true,
handler(loc) {
this.province = loc.province;
this.city = loc.city;
}
}
}
Nested items in your prop location won't be reactive, you'll need to use something like lodash deepClone :
<location :location.sync="_.deepClone(loc)"></location>
That's it, no need for watchers or anything else.

mobx, render when a property value changes

I'm just getting started with Mobx in a react-native project and am having trouble understanding how to perform changes on a observed object.
Changing the object reference via the setWorkingObject action function in my store properly renders the UI, however if I just want to change a single property within this object, how do I cause a render?
My "store":
export default class MyStore {
constructor() {
extendObservable(this, {
workingObject: null
});
}
}
My "container":
class Container extends Component {
render() {
return (
<Provider store={new MyStore()}>
<App />
</Provider>
);
}
}
and my "component", which uses a simple custom input component (think of it like Checkbox) to perform changes to a property of my workingObject
class MyClass extends Component {
...
render() {
const {store} = this.props;
return
<View>
...
<RadioGroup
options={[
{ title: "One", value: 1 },
{ title: "Two", value: 2 }
]}
onPress={option => {
store.workingObject.numberProperty = option.value;
}}
selectedValue={store.workingObject.numberProperty}
/>
...
</View>
}
}
export default inject("store")(observer(MyClass));
I can't figure out why this doesn't work, in fact it looks very similar to the approach used in this example
Any other tips/critique on how I've implemented mobx welcome
The problem is that only existing properties are made observable at the time the workingObject is first assigned.
The solution is to declare future properties at the time of assignment, ie:
// some time prior to render
store.workingObject = { numberProperty:undefined };
First, you don't want to set initial value to null. Second, adding properties to observable object after it was created will not make added properties observable. You need to use extendObservable() instead of assigning new properties directly to observable object. Another solution is to use observable map instead.
in your store:
extendObservable(this, {
workingObject: {}
});
in your component:
extendObservable(store.workingObject, {numberProperty: option.value});
I recommend using Map in this case:
extendObservable(this, {workingObject: new Map()});
in your component:
store.workingObject.set(numberProperty, option.value);